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:

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:

  1. 0<n<128: n détermine le nombre d'une séquence d'octets vides (0)
  2. 128<n<256: n-128 détermine le nombre d'une séquence d'octets pleins (255)
  3. 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).
  4. n==0: deux cas possibles pour l'octet suivant p
    1. 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).
    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.

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é

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é

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()