|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Tout le monde connaît le format IFF. Développé par la société Electronic Arts, il s'est rapidement imposé comme standard de stockage de données sur Amiga. Sa manipulation était très lourde sous AmigaOS 1.3 ; mais Commodore s'est rattrapé avec l'apparition, en même temps que le Kickstart 2.0, de la fameuse iffparse.library. C'est elle que nous allons maintenant étudier. Généralités Le format IFF IFF signifie "Interchange File Format". Son principe est assez simple : sachant que beaucoup de types de données différentes peuvent se trouver dans un même fichier, le mieux est de les présenter par une syntaxe précise, au lieu de définir une structure figée, ce qui aurait forcé une syntaxe différente pour des types de données différentes. C'est pourquoi tout fichier IFF est divisé en blocs de données (alias "chunks"). Ainsi, un bloc contient un flot de données, dont le type est reconnu grâce à un identificateur de quatre lettres, et de longueur connue. Prenons un exemple concret : une chaîne de caractères indiquant le nom de l'auteur d'un échantillon sonore, enregistré dans un bloc de données de type AUTH (pour author=auteur). La structure de ce bloc de données sera alors :
Un fichier IFF est toujours constitué d'une suite de tels blocs de données. Un fichier image sera, par exemple, constitué d'un bloc de données BMHD (BitMap HeaDer) indiquant les caractéristiques de l'image (largeur, hauteur...), un bloc de données CMAP (Color MAP) pour la palette, un bloc de données CAMG (Commodore AMiGa) pour le mode d'affichage, un bloc de données BODY pour les données de l'image proprement dites. Il est impératif qu'un bloc de données commence sur un décalage pair dans le fichier. C'est pourquoi, si le bloc de données précédent contient un nombre d'octets impairs, on rajoute un octet nul, qui n'est évidemment pas comptabilisé dans la longueur. Ceci permet notamment d'accéder à l'identificateur et à la longueur avec des opérations de mot long (évite les gourous désagréables sur un simple 68000 !). Ce n'est pas suffisant. Il y a de nombreux cas où l'on a besoin d'une arborescence (une image incluse dans un texte, par exemple). Ceci est élégamment accompli grâce à des blocs de données spéciaux, nommés FORM. Nous précisons tout de suite qu'on peut exceptionnellement trouver des blocs de données nommés LIST, CATS ou PROP, mais ceux-ci sont peu utilisés. Le rôle des FORM est de regrouper des blocs de données qui ont quelque chose en commun (par exemple, toutes les données constituant une image). Ces blocs de données sont alors inclus dans le bloc de données FORM lui-même, précédés d'un type de quatre lettres, à ne pas confondre avec l'identificateur du bloc de données FORM, qui est justement "FORM", ni avec les identificateurs des blocs de données de la FORM. Ce type sera par exemple ILBM (InterLeaved BitMap) pour une image, PREF pour un fichier de préférences, CTLG (CaTaLoG) pour un catalogue de la bibliothèque locale.library. Un fichier IFF est toujours constitué d'une FORM principale (les autres blocs de données ne sont pas autorisés en dehors de celle-ci). Un fichier image peut donc être constitué ainsi :
Comme en peut s'en douter, trouver un algorithme qui gère tout cela est somme toute assez simple. Les choses se compliquent sérieusement lorsqu'on prend en compte les erreurs qui peuvent intervenir, cas où il faut toujours tester la fin de fichier à chaque lecture, même à l'intérieur d'un bloc de données par exemple, ou si on ne veut pas attacher d'importance à l'ordre des blocs de données (dans notre exemple, le bloc de données BMHD est nécessaire pour décoder le bloc de données BODY, mais ce dernier peut très bien se trouver avant BMHD). Heureusement, nous disposons maintenant de la bibliothèque iffparse.library. Elle peut prendre toutes ces opérations en charge, réclame peu de travail de la part du programmeur. À noter que l'iffparse.library fonctionne également sous Kickstart 1.3 mais ce n'est pas une raison pour continuer à utiliser cet ancêtre. Principes Toutes les constantes et structures de données sont définies dans le fichier d'inclusion libraries/iffparse.i. Ouverture de la bibliothèque L'iffparse.library est une bibliothèque partagée comme on en a l'habitude sur Amiga, elle doit donc être ouverte et fermée comme toutes les autres. Pour l'instant, elle réside sur disque. Il est conseillé d'utiliser la version 36 pour l'ouverture :
Gestion des canaux Un fichier IFF est généralement stocké sur disque (donc par le DOS), mais il se peut que les données viennent d'ailleurs, par exemple du presse-papiers ("clipboards"), ou plus simplement de la mémoire. C'est pour cela que l'iffparse généralise la notion de canaux de données (data stream), utilisés couramment par le DOS. Il est ainsi facilement possible de définir des canaux de toutes les sources imaginables, il suffit d'écrire l'interface nécessaire (routines de lecture et écriture) et de se laisser commander par l'iffparse.library. Toutes les transactions passent donc par une structure spéciale, volontairement nommée "IFFHandle" (rapport aux FileHandles du DOS) :
Il est à noter que cette structure contient d'autres champs, dont l'utilisation est connue de l'iffparse.library seule, et qui doivent bien entendu être initialisés. C'est pourquoi, afin d'assurer une compatibilité future, seule l'iffparse.library est autorisée à allouer une structure IFFHandle. Une telle allocation se fait grâce à la fonction AllocIFF() :
Il faut toujours tester le résultat. Une valeur de retour nulle indique une erreur (en général, c'est un manque de mémoire). Pour libérer la structure IFFHandle, il y a la fonction FreeIFF() :
Comme nous l'avons précisé plus haut, le canal d'entrée-sortie peut être à peu près n'importe quoi. Pour ses opérations, l'iffparse.library a donc besoin de routines extérieures qui lui fournissent les données. Ceci se fait en déclarant un gestionnaire de canal (Stream handler), qui sera attaché à une structure IFFHandle précédemment allouée. Concrètement, on utilise une structure Hook (crochet), que l'on fournit à la fonction InitIFF() (la structure Hook (crochet) est définie dans utility/hooks.i) :
Ici, drapeaux sera une des valeurs suivantes : IFFF_FSEEK (forward seek) si le canal ne permet pas les retours en arrière, ou IFFF_RSEEK (random seek) si le canal permet les retours en arrière. Il est évident que dans le cas d'un canal en forward seek, il peut être nécessaire pour l'iffparse.library (surtout lors d'écritures utilisant IFFSIZE_UNKNOWN, voir plus loin) de réserver des tampons, ce qui se traduit par une utilisation accrue de mémoire. Les fichiers DOS et la mémoire permettent les retours en arrière. Pas le presse-papiers. Le paramètre "streamhook" (crochet de canal) est une structure Hook (crochet) des plus classique :
Le seul champ dont l'initialisation est indispensable est "h_Entry" : il doit pointer sur la routine à appeler. Les conventions d'appel des crochets stipulent qu'alors A0 pointe sur la structure Hook elle-même, A1 sur un "message" dont le format est au choix de l'utilisateur, et A2 un "objet", qui peut, là encore, être n'importe quoi. Le champ h_SubEntry n'a souvent de signification que pour les programmeurs en C : on y stocke le pointeur sur une fonction écrite en C (qui prend ses paramètres sur la pile), h_Entry étant alors un pointeur sur un "stub", c'est-à-dire une petite routine assembleur qui pousse les registres A2,A1 et A0 sur la pile et appelle h_SubEntry. h_Data est laissé à l'usage du possesseur du crochet, qui peut y mettre les données qu'il veut. Cette valeur n'est pas touchée par le système, elle peut être facilement obtenue puisqu'on a l'adresse du crochet en A0. Dans le cas qui nous concerne, l'iffparse.library appelle notre crochet lorsqu'une transaction de données est nécessaire. En A2 (l'objet), on trouve un pointeur sur la structure IFFHandle concernée, et en A1 (le message), on trouve un pointeur sur une structure IFFStreamCmd :
Attention : ci-dessus, c'est la définition de cette structure telle qu'elle est donnée dans les fichiers d'inclusion du C. Il semble que les programmeurs aient ressenti le besoin de changer le préfixe dans les fichiers d'inclusions de l'assembleur (le sc_ est remplacé par isc_). C'est sans doute pour éviter des conflits avec une autre structure. Il en sera de même pour les structures StoredProperty (sp_ remplacé par spr_) et CollectionItem (ci_ remplacé par cit_). Le champ sc_Command contient une valeur qui indique l'opération à effectuer (remarquez la similitude à peine voulue avec les fonctions Open(), Close(), Read(), Write() et Seek() de la dos.library) : IFFCMD_INIT : le canal doit se préparer à envoyer ou recevoir des données. C'est ici qu'il faut accomplir toutes les initialisations préliminaires. Cette commande est utilisée par OpenIFF() (voir plus loin). Cette opération a le droit d'échouer, une valeur de retour (en D0 bien sûr) différente de 0 sera interprétée comme code d'erreur et sera retournée directement au client. IFFCMD_CLEANUP : les transactions sont terminées pour l'instant sur ce canal. Cette valeur est utilisée par CloseIFF(). Cette opération n'a pas le droit d'échouer. IFFCMD_READ : le canal doit lire des données et les placer dans le tampon prévu à cet effet. Ce tampon est pointé par sc_Buf, le nombre d'octets concernés est dans nc_Bytes. L'opération a bien sûr le droit d'échouer. La valeur de retour, si elle est positive, indique le nombre d'octets réellement lus (cette valeur peut être inférieure à nc_Bytes, par exemple dans le cas où la fin de fichier est atteinte). Si elle est négative, c'est un code d'erreur (une des valeurs IFFERR_#?). IFFCMD_WRITE : le gestionnaire (handler) doit écrire dans le canal. Les données à écrire se trouvent dans le tampon indiqué par sc_Buf, leur longueur est dans sc_Bytes. La valeur de retour a la même signification que pour IFFCMD_READ. Un nombre d'octets réellement écrits inférieur à sc_Bytes peut par exemple intervenir dans le cas où une disquette est pleine. IFFCMD_SEEK : l'iffparse.library vous demande de vous positionner dans le fichier. Vous devez vous déplacer du nombre d'octets indiqué par sc_Bytes. Une valeur négative signifie qu'il faut retourner en arrière. Si le canal a été initialisé avec le drapeau IFFF_FSEEK, vous n'aurez jamais de valeur négative. Une valeur de retour non nulle (même positive !) sera interprétée comme une erreur, transformée en IFFERR_SEEK et renvoyée au client. Afin de faciliter toutes ces opérations, le champ iff_Stream de la structure IFFHandle est totalement à la disposition du gestionnaire. On se rend là compte que c'est une technique très puissante. Un petit exemple : vous avez un fichier IFF codé dans votre programme. Pour l'utiliser grâce à l'iffparse.library, vous pouvez écrire un crochet qui a cette forme décrite dans ce fragment de programme (nous avons volontairement omis toutes les initialisations de toutes sortes, n'essayez pas de le taper tel quel !) :
Comme on peut le voir, ce n'est pas bien difficile. Mais c'est quand même assez fastidieux, sachant que la plupart du temps les fichiers résident sur disque. Alors, devra-t-on à chaque fois réécrire le même crochet pour les fichiers venant du DOS ? Nenni, les programmeurs de chez Commodore y ont pensé, et proposent (heureusement, sans cela personne n'utiliserait l'iffparse.library !) des crochets standards pour deux sources courantes : le DOS (ouf) et le presse-papiers. C'est du plus haut intérêt, la gestion de celui-ci n'étant pas particulièrement évidente. C'est à cela que servent les deux fonctions suivantes de l'iffparse.library :
Ces fonctions installent des crochets standards sur les structures IFFHandle données en paramètre (ces fonctions appellent donc InitIFF()). Le fonctionnement de ces crochets est évident en lui-même, la seule chose à savoir est que le champ iff_Stream de la structure IFFHandle doit être initialisé avant l'ouverture du canal (par OpenIFF()). Cette initialisation n'est pas nécessaire au moment de l'appel des fonctions InitIFFas#?(). Le crochet installé par InitIFFasDOS() nécessite dans le champ iff_Stream un BPTR sur une structure FileHandle. Le plus souvent, il s'agira de la valeur retournée par un précédent appel à la fonction Open() de la dos.library. Le fichier devra rester ouvert au moins jusqu'à la fermeture du canal IFF (par CloseIFF()). Il va de soi que le fichier ne doit pas être utilisé par des appels directs à la dos.library entre OpenIFF() et CloseIFF() (sinon, l'iffparse.library ne retrouvera jamais ses billes). Le crochet installé par InitIFFasClip() nécessite dans le champ iff_Stream un pointeur sur une structure ClipboardHandle :
Cette structure contient en fait tout ce qui est nécessaire pour accéder au clipboard.device. On peut l'utiliser d'ailleurs pour accéder directement à celui-ci (et faire des CMD_POST, etc), tant que le canal IFF n'est pas ouvert bien sûr (nous n'entrerons pas dans les détails de la gestion du clipboard.device). Mieux : l'iffparse.library propose une fonction qui ouvre le clipboard.device en s'occupant de toutes les initialisations nécessaires ! Une autre fonction s'occupe bien sûr de le fermer.
Le paramètre pris par OpenClipboard() est un numéro d'unité (entre 0 et 255). Généralement ce sera 0. Attention cependant : les autodocs mentionnent que ces fonctions étaient largement boguées avant la version 39 de l'iffparse.library. Il faudra y faire très attention. D'ailleurs, il semble que la version 39 (fournie avec le Kickstart 3.0) fonctionne parfaitement sous Kickstart 2.0 (et sans doute sous 1.3 bien que nous ne l'ayons pas testé). Nous vous encourageons donc à vous la procurer si ce n'est déjà fait. La valeur retournée par cette fonction est à placer telle quelle dans le champ iff_Stream de la structure IFFHandle (veillez à ne pas faire de confusion entre tous les types de gestionnaires !). Une fois que le IFFHandle a été initialisé, il faut ouvrir le canal. Ceci se fait, comme nous l'avons déjà mentionné, grâce à la fonction OpenIFF() :
Le paramètre "iff" est un pointeur sur une structure IFFHandle précédemment initialisée, et mode indique si le fichier doit être ouvert en lecture (IFFF_READ) ou en écriture (IFFF_WRITE). Cela correspond à peu près aux MODE_OLDFILE et MODE_NEWFILE du DOS. À partir de la version 39, il est dit dans les autodocs que le paramètre "iff" peut être nul, auquel cas l'erreur IFFERR_NOMEM est retournée. C'est bien gentil d'y avoir pensé, cela aurait facilité la vie des programmeurs en C dans la mesure où les fonctions InitIFF#?() accepteraient un paramètre nul. Malheureusement ce n'est pas le cas (du moins si on se fie aux autodocs). On ne peut pas penser à tout. La fonction OpenIFF(), comme nous l'avons précisé plus haut, appelle le message IFFCMD_INIT sur le crochet chargé du canal. Le pendant de OpenIFF() est la bien-nommée CloseIFF() :
Cette fonction termine toutes les opérations associées au canal IFF donné en paramètre. Pour pouvoir être réutilisé, celui-ci doit être rouvert par OpenIFF(). Le message IFFCMD_CLEANUP est appelé sur le crochet gérant le canal. Il y a une petite incompatibilité avec le DOS. En effet, à partir du Kickstart 2.0, il est précisé que la fonction Close() du DOS peut échouer. Cela arrive lorsque des tampons n'ont pas été écrits et que le disque est plein. L'iffparse.library (et donc le crochet qui s'occupe de l'interface avec le DOS) n'a pas prévu ce cas, et il se peut que l'on perde des données. La représentation des blocs de données Comme décrit au paragraphe 1, le format IFF permet une arborescence des blocs de données spéciaux (les FORM) permettant de définir un type et de contenir d'autres blocs de données. L'iffparse.library mémorise les blocs de données grâce à une nouvelle structure (en grande partie privée d'ailleurs). Cette structure se nomme "ContextNode", ses champs connus sont les suivants :
Le champ cn_Node sert à relier de tels ContextNode dans une liste, qui est en fait une pile, associée à la structure IFFHandle en cours. Le champ iff_Depth représente d'ailleurs la profondeur de cette pile (niveau d'arborescence). Nous y reviendrons. Le champ cn_ID est bien évidemment l'identificateur du bloc de données, par exemple "CMAP". Attention, ce n'est pas un pointeur sur une chaîne de caractères mais bien le mot long formé par les codes ASCII des quatre caractères qui y figure. De la même façon, le champ cn_Type contient le type du bloc de données, qui est défini par la FORM qui contient ce bloc de données (ex: "ILBM"). cn_Size représente la taille du bloc de données. cn_Scan représente le nombre d'octets qui ont été parcourus (en lecture ou en écriture), c'est-à-dire la position actuelle dans le bloc de données. Revenons sur cette notion de pile de ContextNode. Imaginons qu'un fichier IFF soit constitué ainsi :
La pile de ContextNode contiendra, lorsque l'iffparse.library sera en train d'examiner le bloc de données BMHD (par exemple !) :
...et iff_Depth@ sera à 3. En fait, ce ne sera pas tout à fait ça car nous avons volontairement omis les champs cn_Node et cn_Scan, sans parler des champs privés (et en plus ces structures ne sont certainement pas contiguës en mémoire). Lorsque l'iffparse.library parcourt un fichier (que ce soit en lecture ou en écriture), deux opérations interviennent sans cesse : celle d'ajouter un ContextNode en haut de la pile (c'est une pile LIFO, Last In First Out), ou de le retirer. La première est nommée "push", l'autre "pop", du nom des deux nouvelles fonctions que nous allons maintenant présenter. Bien qu'utilisables en lecture, elles ne sont généralement utilisées qu'en écriture (souvent, on laisse ParseIFF(), qui utilise d'ailleurs ces fonctions, se charger du mode lecture).
PushChunk() ajoute un bloc de données (un ContextNode) à la pile. En lecture, le ContextNode est créé d'après les informations contenues dans le fichier ; en écriture, il est créé avec les paramètres de la fonction. Le paramètre type n'est réellement utilisé que lorsque id="FORM" (il est ignoré pour les blocs de données feuilles, c'est le type de la FORM parente qui est utilisé). En mode écriture, lorsque le paramètre taille est fourni, toutes les écritures respecteront cette taille. On peut également utiliser la valeur spéciale IFFSIZE_UNKNOWN, auquel cas la taille du bloc de données s'adapte automatiquement aux données qui y sont écrites (cela peut rendre les choses plus lentes, et augmenter la consommation de mémoire, voir plus haut). En retour, erreur vaut 0 si tout s'est bien passé, ou un des codes IFFERR_#? en cas d'erreur.
Cette fonction termine le bloc de données en cours. Elle écrit et libère les tampons qui pouvaient rester, et la structure ContextNode courante est dépilée et détruite. Tous les LocalContextItems (voir plus loin) sont purgés. Là encore, cette fonction n'est réellement utile qu'en écriture. Mentionnons ici les quelques fonctions qui permettent d'accéder aux différentes structures ContextNode stockées sur la pile. Elles sont valables en particulier pendant la lecture et les gestionnaires d'entrées Entry handlers (Cf. plus loin).
La fonction renvoie la structure ContextNode qui se trouve le plus haut sur la pile. C'est le bloc de données qui est en train d'être lu, ou écrit. La valeur de retour est nulle si la pile est vide.
Cette fonction renvoie le contexte père du contexte donné en paramètre. Pour une palette de couleurs (CMAP) contenue dans une image (ILBM) par exemple, ce sera la FORM ILBM. Le résultat est nul si le paramètre est déjà la racine.
Cette fonction, d'après les autodocs, retourne un pointeur sur la structure ContextNode du bloc de données le plus près du haut de la pile capable de contenir d'autres blocs de données. En gros, c'est la FORM la plus près du haut de la pile. On lui fournit en entrée un pointeur sur la structure IFFHandle (qui contient la pile). Le résultat est nul si la pile est vide. Les localcontextitems Voici une notion pas facile à aborder. En fait, c'est le plus difficile à saisir dans l'iffparse.library, mais une fois bien compris, le reste coule de source. Pour simplifier, disons qu'un LocalContextItem est une "chose" (un objet comme disent les programmeurs "orientés objets" :-) qui "vit" dans un ContextNode. Que celui-ci vienne à disparaître, tous ses LocalContextItem disparaissent avec lui. De plus, un LocalContextItem "vit" aussi dans tous les blocs de données fils du bloc de données courant. C'est-à-dire, par exemple, que les LocalContextItem d'une FORM ILBM influencent aussi le bloc de données CMAP inclus dans cette FORM. En cas de conflit, c'est le LocalContextItem le plus près du haut de la pile (le plus "local") qui a la priorité. La structure (du moins sa partie visible) d'un LocalContextItem est la suivante :
On reconnaît une structure MinNode, qui sert à chaîner tous les LocalContextItem dans une liste située dans (la partie privée de) la structure ContextNode. lci_ID et lci_Type identifient les blocs de données à influencer par ce LocalContextItem, et lci_Ident contient encore un identificateur de 4 lettres, minuscules cette fois-ci, indiquant la classe du LocalContextItem. quatre classes sont reconnues par l'iffparse.library - nous les verrons plus loin - mais d'autres sont possibles. L'allocation d'un LocalContextItem se fait par la fonction AllocLocalItem() :
Les trois premiers paramètres sont simplement stockés dans une structure LocalContextItem nouvellement créée, la seule chose nouvelle est ici la quatrième valeur, qui indique la taille de la zone de données à réserver et qui sera stockée dans un des champs "secrets" de la structure LocalContextItem, et accessible par la fonction suivante :
En plus de ce pointeur sur une zone de données, chaque LocalContextItem contient un autre pointeur, appelé vecteur de purge. Ce vecteur pointe sur une fonction (en fait un crochet) qui est appelée lorsque le contexte courant est sur le point d'être retiré. On peut modifier ce vecteur grâce à la fonction suivante :
Ici, les paramètres sont d'une part un pointeur sur une structure LocalContextItem précédemment allouée et un pointeur sur un crochet de retour "callback hook". L'appel de ce crochet suit les conventions habituelles : A0 pointe sur le crochet, A1 (le "message") pointe sur un mot long égal à IFFCMD_PURGELCI, et A2 pointe sur la structure LocalContextItem à libérer. Grâce à cela, on peut ainsi libérer des ressources annexes. En dernier lieu, pour libérer la structure LocalContextItem elle-même, la fonction de purge doit appeler la fonction suivante :
Cette fonction est l'inverse de AllocLocalItem(), elle sert à libérer les ressources allouée par l'iffparse.library pour la structure LocalContextItem donnée en paramètre (cela inclut la libération de la mémoire occupée par la zone de données). Insistons encore une fois sur le fait que cette fonction n'appelle pas le vecteur de purge, c'est lui qui doit l'appeler. Dernière chose : la fonction AllocLocalItem() installe elle-même un vecteur de purge qui est un simple appel à FreeLocalItem(). C'est ce qui se passe si on n'installe pas d'autre vecteur de purge. Maintenant que nous avons créé une structure LocalContextItem, il reste à l'attacher à un contexte. Il existe deux fonctions pour cela :
"iff" est bien sûr un pointeur sur la structure IFFHandle. "item" est un pointeur sur la structure LocalContextItem. "position" est un code spécial, qui dit où stocker le LocalContextItem. Les valeurs possibles de position sont :
N'oubliez pas qu'une fois le LocalContextItem lié au ContextNode, il disparaîtra en même temps que celui-ci (via le vecteur de purge). De plus, si dans le même ContextNode il existe un LocalContextItem de mêmes type, ID et classe, celui-ci sera purgé et remplacé par le nouveau. À quoi cela servirait-il si l'on n'avait pas une fonction pour trouver les LocalContextItems ? La voici :
Cette fonction scrute, depuis le haut (le bloc de données courant) jusqu'en bas (la racine), la pile de ContextNode associé à la structure IFFHandle à la recherche d'un LocalContextItem possédant les type, ID et classe donnés en paramètres. Le pointeur sur sa structure est retourné en d0, 0 si aucun n'a été trouvé. Voilà, c'est tout pour les LocalContextItems. Si vous avez bien compris, le reste ne devrait pas poser de problème (mais ça ne veut pas dire que c'est terminé !). Tant que nous y somme, nous casons ici trois fonctions qu'on pourrait difficilement mettre ailleurs :
Ces fonctions testent respectivement si un mot long peut être utilisé comme identificateur de bloc de données ou comme type de FORM (norme EA IFF 85). En retour, on obtient une valeur booléenne.
Cette fonction transforme un mot long (un ID ou un type) en chaîne de caractères, stocké dans le tampon, qui doit avoir au moins cinq octets. Elle est surtout utile pour les programmeurs en C, pour qui les chaînes de caractères doivent avoir un 0 terminal. Explications C'est bien beau de connaître la structure des blocs de données d'un fichier, ou d'en créer une, mais n'oublions pas que ces blocs de données doivent contenir des données. Voyons donc les fonctions qu'offre l'iffparse.library pour cela. Lecture et écriture La lecture se fait toujours (c'est valable aussi pour l'écriture) dans le bloc de données courant, c'est-à-dire depuis le ContextNode du haut de la pile.
Cette fonction lit (à partir de la position courante dans le bloc de données) un nombre donné d'octets (spécifié par le paramètre longueur) et les place dans le tampon pointé par A1. Cette méthode appelle bien sûr IFFCMD_READ sur le gestionnaire de canal stream handler. Le nombre retourné est soit, s'il est positif, le nombre d'octets réellement lus (qui peut être inférieur à celui demandé, dans le cas où la fin du bloc de données est atteinte), soit, s'il est négatif, un code d'erreur. Nous en profitons pour vous décrire ici les différents codes d'erreur gérés par l'iffparse.library :
Mais revenons à l'iffparse.library, pour vous présenter la seconde fonction de lecture, sans doute beaucoup moins utilisée :
Grâce à cette fonction, on peut lire une suite d'enregistrements de taille fixe depuis le canal iff dans le tampon. La taille des enregistrements est dans longRecord, leur nombre dans nbRecords. Si la fin du bloc de données est atteinte, la lecture est tronquée au dernier enregistrement entier lu. Cette fonction peut être utile, par exemple si un bloc de données contient une série de petites structures de même taille. En retour, nombre indique le nombre d'enregistrements entiers lus (si positif ou nul), ou un code d'erreur (négatif). Il y a bien entendu les deux fonctions d'écriture correspondantes, de même syntaxe, les paramètres ont la même signification :
La seule précision à apporter est que toutes les opérations d'écritures respecteront la taille maximale fournie lors de PushChunk(). Si cette taille était IFFSIZE_UNKNOWN, alors les opérations d'écriture ajusteront automatiquement cette taille. Les gestionnaires entry/exit Deux nouvelles bêtes à l'étude : les "Entry handlers" (gestionnaires d'entrée) et "Exit handlers" (gestionnaires de sortie). Ce sont des LocalContextItem, de classes (ident) respectives "enhd" et "exhd". Ils sont spéciaux car l'iffparse.library (et plus précisément la fonction ParseIFF() que nous gardons pour la fin) leur réserve un traitement spécial. Ils ne sont utiles qu'en lecture, évidemment. Les zones de donnée de ces LocalContextItem sont des crochets (eh oui, encore des crochets), appelés respectivement à l'entrée d'un contexte (nouveau bloc de données) et à la sortie d'un contexte (fin de bloc de données). Précisons tout de suite que ces gestionnaires n'affectent que les blocs de données de même type et ID que le gestionnaire lui-même (évidemment, puisqu'ils sont localisés par FindLocalItem(), avec les arguments cn_ID et cn_Type du contexte courant et l'ident "enhd" ou "exhd"). Vous voyez tout de suite l'intérêt : en plaçant sur la racine d'un fichier IFF un gestionnaire différent pour les blocs de données ILBM CMAP, ILBM BMHD, ILBM BODY et ILBM CAMG, il devient vraiment aisé de lire et afficher une image IFF ! Les gestionnaires étant des LocalContextItem à part entière, tout ce que nous avons dit au paragraphe 2.4 reste valable, notamment en ce qui concerne la priorité descendante. Ainsi, ou pourra installer des gestionnaires différents (mais visant le même bloc de données) à différents niveaux : racine, FORM principale, FORM secondaire, etc. pour une puissance assez étonnante. Voyons comment installer un gestionnaire d'entrée :
Cette fonction attache au canal IFF un gestionnaire d'entrée de type et id donnés (ident sera automatiquement mis à "enhd"), à la position spécifiée (voir la fonction StoreLocalItem() au paragraphe 2.4 pour les valeurs possibles). Ce gestionnaire est matérialisé par un crochet (qui serait recopié dans la zone de données du LocalContextItem). Lors de l'appel de ce crochet (lorsqu'un nouveau bloc de données vient d'être détecté), on trouvera les paramètres habituels : A0 pointant sur la structure de crochet, A1 pointant sur un mot long contenant IFFCMD_ENTRY, et A2 sera la valeur donnée ici comme paramètre "objet". A ce stade, le pointeur du canal est positionné au début du bloc de données, on peut lire des données grâce à ReadChunk#?(). La valeur de retour du crochet sera interprétée comme suit :
Les paramètres sont les mêmes que pour la fonction EntryHandler(). La différence est que lors de l'appel du crochet, A1 pointe sur un mot long égal à IFFCMD_EXIT, et que le pointeur du canal IFF est quelque part entre le début et la fin du bloc de données (on ne sait pas où à l'avance). N'oubliez pas que, comme tous les LocalContextItem, les gestionnaires d'entrée et de sortie meurent avec le ContextNode dans lequel ils sont stockés (les LocalContextItem stockés sur la racine ne sont détruits que par FreeIFF(), ils subsistent même après CloseIFF()). Les gestionnaires prédéfinis L'iffparse.library nous propose, pour nous faciliter la vie, des gestionnaires d'entrée et de sortie prédéfinis. Voyons-les un par un.
Ceci installe un gestionnaire d'entrée dont la seule fonction est de renvoyer IFF_RETURN2CLIENT. Ainsi, lorsque ParseIFF() rencontre un tel bloc de données, elle s'arrête et renvoie 0. Rien de plus simple.
Encore un gestionnaire d'entrée, un peu plus compliqué. Son but est de créer un LocalContextItem contenant le bloc de données lui-même. Ce LCI aura ses type et ID égaux à ceux du bloc de données en question, son ident sera "prop". Le plus intéressant est bien sûr la zone de données : elle contient une structure StoredProperty :
Cette structure (rappel : dans les fichiers d'inclusions INCLUDES C, le préfixe est "sp", alors qu'il est "spr_" dans les fichiers d'inclusion assembleur) indique où sont stockées les données du bloc de données, ainsi que leur longueur. Le désassemblage de l'iffparse.library montre qu'actuellement, les données sont stockées juste après cette structure, mais il n'en sera pas forcément ainsi dans les versions futures de l'iffparse.library. Dernière chose, le LCI est stocké avec la position IFFSLI_PROP, c'est-à-dire dans la FORM parente (et donc disparaît avec celle-ci). Ce processus est automatique (le crochet laisse ParseIFF() se poursuivre en paix), donc il faut pouvoir retrouver les blocs de données après. C'est à cela que sert la fonction FindProp() :
Cette fonction est en fait équivalente à :
Le gestionnaire installé par cette fonction est presque identique à celui installé par PropChunk(). La différence est que CollectionChunk() autorise de multiples blocs de données à être stockés, alors que si le crochet de PropChunk() rencontre deux fois le même bloc de données, le premier est purgé (puisque c'est un LCI à part entière). Ici, lorsque le gestionnaire rencontre un bloc de données qui l'intéresse, il le lit comme le ferait le gestionnaire de PropChunk(), mais au lieu d'écraser un bloc de données de ces type et ID déjà recopiés, il le lie à ce dernier, formant ainsi une liste de structures nommées CollectionItem :
C'est un raccourci pour :
Voilà tout pour les gestionnaires d'entrée prédéfinis. Il y a un gestionnaire de sortie connu de l'iffparse.library :
Ce gestionnaire arrête la fonction ParseIFF() après (au moment où le ContextNode va disparaître, voir plus haut) la rencontre d'un bloc de données de type et ID souhaités (en renvoyant IFF_RETURN2CLIENT). Nous en verrons une utilisation dans l'exemple ci-après. Dernière chose : pour les fonctions installant des gestionnaires d'entrée, c'est-à-dire StopChunk(), PropChunk() et CollectionChunk(), il existe des versions "multiblocs de données": elles évitent d'utiliser plusieurs fois de suite ces fonctions pour tous les blocs de données nécessaires. En fait, il s'agit juste de raccourcis accomplissant une boucle pour tous les blocs de données de la table. Elles se nomment respectivement StopChunks(), PropChunks() et CollectionChunks() et ont toutes le même prototype :
Le paramètre "table" est ici un pointeur sur un tableau de paires de mots longs, contenant type et id des blocs de données concernés. Le nombre de telles paires se trouve dans le paramètre paires. La fonction ParseIFF() Là, nous avons vraiment gardé le meilleur pour la fin. Il s'agit d'une fonction, ParseIFF(), qui vous permettra d'analyser très facilement tous vos fichiers IFF.
Cette fonction est bien sûr utilisable en mode lecture uniquement. Son rôle est de traverser un fichier IFF (déjà ouvert par OpenIFF() bien sûr), en "pushant" et "poppant" (ces termes sont expliqués au paragraphe 2.3) les ContextNodes alors que les blocs de données commencent et se terminent dans le fichier. À chaque fois (sauf en mode IFFPARSE_RAWSTEP, voir plus loin), que ParseIFF() effectue un PushChunk() (en lecture, rappelons-le, les données sont prises depuis le fichier), elle cherche (par FindLocalItem()) un LocalContextItem correspondant (mêmes type et ID) d'ident "enhd", s'il y en a, c'est un gestionnaire d'entrée, qui est alors appelé. De la même façon, juste avant de retirer un ContextNode (juste avant PopChunk()), ParseIFF() cherche un LocalContextItem correspondant avec ident="exhd". Ce gestionnaire de sortie est alors appelé. N'est-ce pas cela la magie de l'Amiga ? Le paramètre "iff" est bien sûr un pointeur sur la structure IFFHandle (indispensable pour ce genre de choses), et le paramètre contrôle contient un code, indiquant à ParseIFF() comment procéder. Les valeurs possibles sont les suivantes :
Programme d'exemple Voilà, cette analyse de l'iffparse.library est terminée, toutes les fonctions ont été passées en revue, nous espérons que vous n'avez pas été rebutés par les aspects rébarbatifs de certains sujets. Nous pouvons maintenant écrire un programme d'exemple. Nous vous proposons un afficheur d'image ILBM très rudimentaire. Il n'arrive pas à la cheville de PPShow ou ViewTek, mais c'est juste un exemple... Le plus gros défaut est sans doute qu'il ne teste pas le débordement dans la décompression du bloc de données BODY, ce qui pourrait arriver dans le cas d'un fichier IFF (très) corrompu. Il fait juste confiance au bloc de données BMHD pour la taille de l'écran. On procède de la manière suivante : une fois toutes les bibliothèques ouvertes, on alloue une structure IFFHandle, on ouvre le fichier DOS, et on déclare les blocs de données BMHD, CAMG, CMAP et BODY pour qu'ils soient stockés lors de l'analyse (par PropChunks()). On installe ensuite un gestionnaire de sortie sur la FORM elle-même, qui sera donc appelé à la fin du fichier. Ce gestionnaire retrouve les blocs de données sus-mentionnés grâce à FindProp(), ouvre un écran de bonnes dimension et résolution, change les couleurs, décode et éventuellement décompresse le bloc de données BODY. Quand vous aurez assez admiré l'image, vous pourrez appuyer sur "Ctrl-C" pour arrêter le programme (du moment que la fenêtre du CLI est toujours active). Ce programme ne prend pas en compte les modes AGA. Libre à vous de faire l'adaptation. Le seul bon moyen d'afficher des images est d'utiliser la bibliothèque datatypes.library. Veuillez noter que nous avons écrit ici un afficheur d'images en ordre de marche pour la bagatelle de 1016 octets (PPShow fait environ 40 ko).
Note : retrouvez le guide, le fichier source et exécutable concernant cet article dans cette archive.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||