Quelques formats d'images des ATARI ST(E)
IL existait pour l'Atari ST quelques formats d'image assez simples, dont le format de fichier .IMG (compressé), très importants puisque mis au point par Digital Research, dont l'interface graphique GEMGraphical Environment Manager était implémentée dans le système. Les logiciels GEM Paint, GEM Scan et Ventura Publisher (MSDOS puis Windows) l'utilisaient.
Pour les images en format écrans (Degas, NEOchrome, Tiny), voir ici.
La solution libre recoil semble difficilement compilable. Une page html permet de lire beaucoup d'images du monde Atari (et Amiga).
Les données sont parfois écrites en notation hexadécimale, où les «chiffres» valent de 0 à 15 (F) et tout rang à gauche vaut 16 fois plus: B9 vaut 11*16 + 9 = 185 ; 123 vaut 256 (1*16*16) + 32 (2*16) + 3 (3*1) = 291. Voici les correspondances hexadécimal-binaire:
0=0000: …. 1=0001: …x 2=0010: ..x. 3=0011: ..xx |
4=0100: .x.. 5=0101: .x.x 6=0110: .xx. 7=0111: .xxx |
8=1000: x… 9=1001: x..x A=1010: x.x. B=1011: x.xx |
C=1100: xx.. D=1101: xx.x E=1110: xxx. F=1111: xxxx |
Il faut deux chiffres hexadécimaux pour le composer un octet, qui vaut de 0 à 255: 202= 12*16+10, soit CA, qui se traduit en monochrome par la suite de pixels xx..x.x.
Format d'un fichier .img
L'entête d'un fichier .img comprend un nombre variable de mots:
- toujours 00 01 (l'entier 1) comme mot de signature
- la taille en mots pour la partie entête
- en monochrome, cela peut être 00 08 ou 00 09 (16 ou 18 octets)
- pour 4 plans, cela peut être 00 3B (59 mots, soit 118 octets) ou 00 08 (16 octets)
- pour 8 plans, cela peut être 03 0B (1558 octets) ou 00 08 (16 octets)
- pour 16 plans, les couleurs pourraient être codées sur 16 bits (5 pour les composantes 'rouge' et 'bleu', 5 ou 6 pour 'vert') - je ne dispose pas d'un tel fichier)
- l'unique fichiers en 24 plans dont je dispose ne possède que 8 mots d'entête, c'est à priori normal, les couleurs pouvant être codées 24 plans. Tentative quand j'ai le temps.
- le nombre de plans
- 00 01: 1 pour le monochrome (en fait bi-level, deux niveaux)
- 00 02: 2 pour 4 couleurs (moyenne résolution du ST)
- 00 04: 4 pour les 16 couleurs de la basse résolution du ST(E) ou moyenne résolution du TT
- 00 08: 8 pour 256 couleurs (moyenne résolution du TT)
- 00 10: 16 pour la True color du Falcon
- 00 18: 24 pour 16 millions de couleurs.
- le nombre d'octets pour une répétition d'octet(s) (voir compression, point 4a) ; le plus souvent 1 ou 2.
- la largeur d'un pixel en microns (millième de millimètres)note
- la hauteur d'un pixel en microns (millième de millimètres)note
- la largeur de l'image en pixels
- la hauteur de l'image en pixels
- un éventuel mot obscur, qui n'est pas utilisé par toutes les applications
- XIMG suivie d'une définition en trois mots pour les composantes de chacune des 16 ou 256 couleurs (4 ou 8 plans)
note L'écran monochrome SM124 de l'Atari ST a une résolution de 72dpi (points par pouce), les pixels ont donc a priori une dimension de 25,4mm÷72, soit 353µ de côté (un tiers de millimètre). Les dimensions de pixels les plus communes des images .img semblent aller de 85 à 385. Attention: un pixel n'est pas nécessairement carré: en moyenne résolution ST (640x200), il est deux fois plus haut que large, en basse résolution TT (320x480), deux fois plus large que haut.
Le reste du fichier sont les données, compressées selon les conditions ci-après.
Formats de compression d'un fichier .img
Les données compressées se lisent octet par octet, ligne après ligne. Chaque ligne est composée d'un nombre entier d'octets. Si la largeur en pixels du fichier n'est pas égale à un multiple de 8, le reste du dernier octet n'est pas pris en compte.
Pour un octet n suivi d'éventuels octets p et q:
- 0<n<128: n détermine le nombre d'une séquence d'octets vides (0)
- 128<n<256: n-128 détermine le nombre d'une séquence d'octets pleins (255)
- n==128: p détermine le nombre d'octets à considérer tels quels. L'octet 80 est donc assez fréquent sauf si l'image contient surtout de grandes plages régulières (blancs, noirs ou de succession régulière de pixels noirs et de pixels blancs).
- n==0: deux cas possibles pour l'octet suivant p
- p>0: p fois un octet ou une séquence d'octets ; le nombre défini au mot 4 de l'entête donne le nombre d'octets à considérer:
- s'il vaut 2 - 00 04 FC A0 code FC A0 FC A0 FC A0 FC A0
- s'il vaut 1 - 00 04 FC A0 code FC FC FC FC, A0 code alors une séquence suivante: 160-128 octets FF (point 2).
- p==0 et q==255: celui qui suit encore donne le nombre de fois que la ligne qui suit sera à répéter: 00 00 FF 12 répétera 18 fois la ligne qui sera composée (souvent un ensemble d'octets blancs si non nul et inférieur à 128, ou noir si supérieur à 128.
- p>0: p fois un octet ou une séquence d'octets ; le nombre défini au mot 4 de l'entête donne le nombre d'octets à considérer:
Si l'image est très complexe (ensemble de points au hasard), chaque ligne doit être préfixée de 80 nn (point 3) et ajoutera donc deux octets à chaque ligne de données. Il en résulterait une image légèrement plus grande qu'un original non compressé. Pour que la compression soit efficace, l'image doit comporter des lignes d'octets blancs 00 (point 1) ou noir FF (point 2) répétables (point 4b), soit les motifs 10001000 10001000 ou 10101010 10101010 (point 4a) typiques des images monochromes tentant de révéler des valeurs de gris.
Couleurs
En monochrome, chaque octet définit 8 pixels (0 est blanc, 1 est noir).
Pour la couleur, il faut prévoir autant de lignes qu'il y a de plans annoncés au mot 3 de l'entête. C'est la combinaison de bits au même rang de chaque plan qui détermine la couleur des pixels (voir ici).
11001100 11001100 premier plan 10101010 10100… second plan: *2 31203120 3… détermine une des 4 couleurs que 2 plans permettent (moyenne résolution ST)
11111111 00000000 premier plan 11110000 11110000 second plan: *2 11001100 11001100 troisième plan: *4 10101010 10101010 quatrième plan: *8 FEDCBA98 76543210 détermine une des 16 couleurs pour 4 plans (basse résolution ST ou moyenne TT)
Les images en quatre plans (16 couleurs) ont une entête de x3B mots (118 octets) si elles définissent les 16 couleurs. Chaque composante (respectivement rouge, vert et bleu) est codée de 0 à x3E4 (996), ce qui fait 6 mots par couleur.
00 01 entête GEM raster 00 3B entête élargi pour la définition de couleur 00 04 quatre plans (16 couleurs) 00 02 schéma de répétitions d'octet(s): par deux 01 74 01 74 dimensions en microns d'un pixel 01 40 01 80 dimension de l'image en pixels 58 49 4D 47 "XIMG" au décalage 16 00 00 ? 00 00 00 00 00 00 trois composante à 0: noir 03 E4 03 E4 03 E4 = 996 996 996: blanc 03 E4 00 00 00 00 = 996 0 0: rouge etc. 94 80 02 F0 début des données au décalage 118
On retrouve la valeur de chaque composante avec couleur*256//1000 (arrondi par défaut), ou mieux int(round(float(couleur)*256/1000)) (la division est a priori entière)
Script pour transformer un fichier .IMG monochrome en .pnm
#! /usr/bin/python # attention: python2!!! # -*- coding:utf8 -*- import sys, os repertoire=os.listdir(".") for i in range(len(repertoire)-1,-1,-1): print repertoire[i] if repertoire[i][-4:].upper() != (".IMG"): repertoire.pop(i) repertoire=sorted(repertoire) for i in range(len(repertoire)): print "%3d %s" %(i, repertoire[i]) q=raw_input("numéro de fichier à transformer: ") nom=repertoire[int(q)] print nom han=open(nom) img=han.read() han.close() def word(x): # transform a two characters string into a 16bits word (up to 65535) return ord(x[0])*256+ord(x[1]) def getcar(n): global data, ofs octet=ord(data[n]); ofs+=1 return octet def getstr(nbr): global ofs, data chaine=data[ofs:ofs+nbr]; ofs+=nbr return [ord(x) for x in chaine] magic=word(img[0:2]) if magic!=1: print "not a GEM img"; sys.exit() plans=word(img[4:6]); print "plans:", plans if plans!=1: print "not a monochrom img"; sys.exit() entete=word(img[2:4]); print "header:", entete clusters=word(img[6:8]); print "clusters:", clusters # number of repeated bytes # [8:12] pixel dimensions (screen resolution, 72 dpi?) largeur=word(img[12:14]); print "width:", largeur hauteur=word(img[14:16]); print "height:", hauteur larg, reste=divmod(largeur, 8) if reste: larg+1 # byte number by ligne (>= line width) data=img[entete*2:] ligne=[]; ofs=0; new="" nbl=1 # line repetition number: 1 by default while ofs<len(data): octet=getcar(ofs) if octet==0: # multplication: octet=getcar(ofs) if octet: # 1st: sequence multiplication nbc=octet # identical byte number to consider stock=[] for i in range(clusters): octet=getcar(ofs) stock+=[octet] ligne+=stock*nbc # product else: # line multiplication octet=getcar(ofs) if octet==255: # arbitrary number nbl=getcar(ofs) # line multiplication number else: if octet==128: # octet=getcar(ofs) ligne+=getstr(octet) elif octet<128: # ligne=ligne+[0]*octet else: ligne=ligne+[255]*(octet-128) if len(ligne)>=larg: newline="" for z in range(len(ligne)): char=ligne[z] for j in range(7,-1,-1): if z*8+(8-j)>largeur: continue p=2**j bit=(1-(char & p)/p)*7 newline+=chr(bit)*3 new+=newline*nbl nbl=1; ligne=[] header="P6 %d %d 7\n" %(largeur, hauteur*plans) han=open(nom+".pnm", "w") han.write(header+new) han.close() #try: # os.system("convert %s.pnm %s.png" %(nom, nom)) # os.system("rm %s.pnm" %nom) # print " %s.pnm converted into %s.png" %(nom, nom) #except: # print " no .png conversion; on UNIX, IMageMagic 'convert' should be present"
Script pour transformer un fichier .IMG 16 couleurs en .pnm
Script à améliorer: certaines images ne passent pas, et il n'y a pas de filtre pour n'accepter que les imags à 4 plans. Il est également possible que les couleurs par défaut ne soient pas les bonnes (en cas d'entête sans définition de couleurs): j'ai mis l'ordre des couleurs «hardware», peut-être l'ordre «VDI» convient-il mieux (voir ici).
#! /usr/bin/python # attention: python2!!! # -*- coding:utf8 -*- import sys, os repertoire=os.listdir(".") for i in range(len(repertoire)-1,-1,-1): print repertoire[i] if repertoire[i][-5:].upper() != (".IMG"): repertoire.pop(i) repertoire=sorted(repertoire) for i in range(len(repertoire)): print "%3d %s" %(i, repertoire[i]) q=raw_input("numéro de fichier à transformer: ") nom=repertoire[int(q)] print nom han=open(nom) img=han.read() han.close() def word(x): # transform a two characters string into a 16bits word (up to 65535) return ord(x[0])*256+ord(x[1]) def getcar(n): global data, ofs octet=ord(data[n]); ofs+=1 return octet def getstr(nbr): global ofs, data chaine=data[ofs:ofs+nbr]; ofs+=nbr return [ord(x) for x in chaine] magic=word(img[0:2]) if magic!=1: print "not a GEM img"; sys.exit() plans=word(img[4:6]); print "plans:", plans if plans!=4: print "not a four-planes img"; sys.exit() entete=word(img[2:4]); print "header:", entete clusters=word(img[6:8]); print "clusters:", clusters # number of repeated bytes # [8:12] pixel dimensions (screen resolution, 72 dpi?) largeur=word(img[12:14]); print "width:", largeur hauteur=word(img[14:16]); print "height:", hauteur # hardware colour definition clrs=[[15,15,15],[0,0,0],[15,0,0],[0,15,0],[0,0,15],[8,0,0],[8,4,0],[0,8,0],[10,10,10],[4,4,4],[0,15,15],[0,8,8],[15,0,15],[8,0,8],[15,15,0],[8,8,0]] # VDI order to add def mot(ch2): return ord(ch2[0])*256+ord(ch2[1]) ximg=img[16:20] if ximg!="XIMG": print "not a 'XIMG' - IMG raster couleur" mx=15 else: clrs=[]; mx=255 for i in range(16): deb=22 red=mot(img[deb+i*6:deb+2+i*6])*256/1000 green=mot(img[deb+2+i*6:deb+4+i*6])*256/1000 blue=mot(img[deb+4+i*6:deb+6+i*6])*256/1000 print red, green, blue clrs+=[[red,green,blue]] larg, reste=divmod(largeur, 8) if reste: larg+1 # byte number by ligne (>= line width) data=img[entete*2:] ligne=[]; ofs=0; new="" nbl=1 # line repetition number: 1 by default newline=[[],[],[],[]]; plan=0 tout="" while ofs<len(data): octet=getcar(ofs) if octet==0: # multplication: octet=getcar(ofs) if octet: # 1st: sequence multiplication nbc=octet # identical byte number to consider stock=[] for i in range(clusters): octet=getcar(ofs) stock+=[octet] ligne+=stock*nbc # product else: # line multiplication octet=getcar(ofs) if octet==255: # arbitrary number nbl=getcar(ofs) # line multiplication number else: if octet==128: # for a bytes sequence octet=getcar(ofs) # how many bytes to consider ligne+=getstr(octet) elif octet<128: # how many white (0) bytes ligne=ligne+[0]*octet else: # how many black (255) bytes ligne=ligne+[255]*(octet-128) if len(ligne)>=larg: newline[plan]=ligne; ligne=[] if plan==3: total="" for z in range(len(newline[0])): char0=newline[0][z] char1=newline[1][z] char2=newline[2][z] char3=newline[3][z] for j in range(7,-1,-1): if z*8+(8-j)>largeur: continue p=2**j bit0=(char0 & p)/p; bit1=(char1 & p)/p bit2=(char2 & p)/p; bit3=(char3 & p)/p clr=bit0+bit1*2+bit2*4+bit3*8 total+=chr(clrs[clr][0])+chr(clrs[clr][1])+chr(clrs[clr][2]) new+=total*nbl nbl=1; newline=[[],[],[],[]] plan=(plan+1)%4 header="P6 %d %d %d\n" %(largeur, hauteur, mx) han=open(nom+".pnm", "w") han.write(header+new) han.close()
BITBLT sur Omikron BASIC
Le BASIC Omikron 3.01 connaissait un format de BIT BLock Transfert (BITBLT) non compressé
- premier mot: 0 pour la basse résolution, 1 pour la moyenne, 2 pour la haute (monochrome)
- second mot: largeur en pixels du bloc
- troisième mot: hauteur en pixels du bloc
Suivent les données non compressées. Les bits des octets non complets sont ignorés. Les lignes de données doivent en outre être codées sur un nombre pair d'octets: 16 bits tiennent sur 2 octets, mais 17 sur 4. Une façon de déterminer un nombre pair d'octets par excès à partir d'un nombre de bits: npox=((nb-1)//16)*2 +2
Voici le script en BASIC Omikron (sur l'émulateur Hatari) ayant fabriqué et sauvegardé le bloc-image OMBAS301.BBT:
CLIP 0,0,639,399 TEXT STYLE =16: TEXT HEIGHT =48 ' style =16 pour outline TEXT 100,100,"Omikron Basic" TEXT STYLE =0: TEXT HEIGHT =36 ' style=0 pour normal TEXT 109,130,"…on Atari ST" Adr= MEMORY(6+30*65) ' reservation de memoire BITBLT 98,72,235,65 TO Adr BSAVE "OMBAS301.BBT",Adr,30*65+6 ' sauvegarde de la memoire vers le disque
Voici le script python qui transforme le BBT en fichier image .pnm
#! /usr/bin/python # attention: python2!!! # -*- coding:utf-8 -*- nom=raw_input("filename: ") han=open(nom) img=han.read() han.close() def word(w): return ord(w[0])*256+ord(w[1]) rez=word(img[0:2]) larg=word(img[2:4]); largo=((larg-1) //16)*2+2 haut=word(img[4:6]) data=img[6:] print nom+".pad - length", len(data), "- height:", haut, "- width:", larg, "px -", largo, "words / line" target="" for j in range(haut): ofs=j*largo for i in range(largo): mot=ord(data[ofs+i]) for k in range(7,-1,-1): if i*8+7-k<larg: if mot & 2**k: target+="1" else: target+="0" header="P1 %s %s\n" %(larg, haut) han=open(nom+".pnm","w") han.write(header+target) han.close()
.pad ()
Il s'agit d'une image en format non compressé
- premier mot +1: largeur en pixels du bloc
- second mot +1: hauteur en pixels du bloc
- troisième mot: nombre de plans?
Suivent les données non compressées. Les bits des octets non complets sont ignorés. Les lignes de données doivent en outre être codées sur un nombre pair d'octets: 16 bits tiennent sur 2 octets, mais 17 sur 4. Une façon de déterminer un nombre pair d'octets par excès à partir d'un nombre de bits: npox=((nb-1)//16)*2 +2
00 0D 00 0C 00 01 0B 43 0B 40 0B 40 0B 40 1B 60 1B 60 3B 70 73 3B F3 3E E3 1C E3 1C C3 0C 83 04
premier mot: D=13, +1 = 14 pixels de largeur
second mot: C=12, +1 = 13 pixels de hauteur
troisième mot: 1 = monochrome
La largeur en pixels étant de 14, deux octets sont nécessaires par ligne. Le deux derniers bits du dernier octet de chaque ligne peut être marqués (représentés par o), mais on n'en tient pas compte. Le graphique qui suit marque la séparation entre chiffres hexadécimaux, plus la dernière colonne cachée.
0B 43 = …. x.xx .x.. .. oo 0B 40 = …. x.xx .x.. .. .. 0B 40 = …. x.xx .x.. .. .. 0B 40 = …. x.xx .x.. .. .. 1B 60 = …x x.xx .xx. .. .. 1B 60 = …x x.xx .xx. .. .. 3B 70 = ..xx x.xx .xxx .. .. 73 3B = .xxx ..xx ..xx x. oo F3 3E = xxxx ..xx ..xx xx o. E3 1C = xxx. ..xx …x xx .. E3 1C = xxx. ..xx …x xx .. C3 0C = xx.. ..xx …. xx .. 83 04 = x… ..xx …. .x ..
Voici un script de transformation de ces images monochromes:
#! /usr/bin/python # attention: python2!!! # -*- coding:utf-8 -*- nom=raw_input("filename (without '.pad'): ") han=open(nom+".pad") img=han.read() han.close() def word(w): return ord(w[0])*256+ord(w[1]) larg=word(img[0:2])+1; largo=((larg-1) //16)*2+2 haut=word(img[2:4])+1 plans=word(img[4:6]) data=img[6:] print nom+".pad - length", len(data), "- height:", haut, "- width:", larg, "px -", largo, "words / line" target="" for j in range(haut): ofs=j*largo for i in range(largo): mot=ord(data[ofs+i]) for k in range(7,-1,-1): if i*8+7-k<larg: if mot & 2**k: target+="1" else: target+="0" header="P1 %s %s\n" %(larg, haut) han=open(nom+".pnm","w") han.write(header+target) han.close()