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.
IMG (GEM) - PSC (PaintShop Compressed) - BBT (Omikron Basic) - PAD
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, qui résume des états de pixels allumés ou éteints, donc binaires. Voici un table de correspondance entre notations décimale, binaire et hexadécimale. Les chiffres binaires sont 0 ou 1 , et tout rang à gauche vaut deux fois plus. Les «chiffres» hewadécimaux valent de 0 à 15 (de 0 à 9 et de A à F) ; tout rang à gauche vaut 16 fois plus : B9 vaut 11 *16 + 9 = 185 ; 123 écrit en hexadécimal vaut 256 (1 *16 *16) + 32 (2 *16) + 3 (3 *1) = 291. Voici les correspondances décimal-binaire-hexadécimal- :
Déc. | Bin. | Hex. | Déc. | Bin. | Hex. | Déc. | Bin. | Hex. | Déc. | Bin. | Hex. | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0000 | 0 | 4 | 0100 | 4 | 8 | 1000 | 8 | 12 | 1100 | C | |||
1 | 0001 | 1 | 5 | 0101 | 5 | 9 | 1001 | 9 | 13 | 1101 | D | |||
2 | 0010 | 2 | 6 | 0110 | 6 | 10 | 1010 | A | 14 | 1110 | E | |||
3 | 0011 | 3 | 7 | 0111 | 7 | 11 | 1011 | B | 15 | 1111 | F |
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 1100 1010
↑ Format d'un fichier .img
Les fichiers IMG disposent d'une compression sans perte. Leur entête 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.
Algorithme 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 un 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 quatrième mot 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 (1 *16 +2) 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 quatrième mot 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 un fichier légèrement plus lourd que l'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)) (en python2, la division est a priori entière)
Script en python3 pour transformer un fichier .IMG monochrome en .pnm
#! /usr/bin/python3 # From GEM IMG Atari monochrome image to PNM / PNG # Jean-Christophe Beumier - 2015.02.06 - GPLv3 # Translated into python3 - 2023.04.14 # https://www.gnu.org/licenses/gpl-3.0.en.html import sys, os # os for listdir() ; sys for exit() 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 =input("numéro de fichier à transformer: ") nom =repertoire[int(q)] print(nom) with open(nom, "rb") as fd : img =fd.read() img =list(img) def word(x) : # transform two bytes into a 16bits word (up to 65535) return x[0] *256 +x[1] def getcar(n) : # transform a character into a byte (up to 255) global data, ofs octet =data[n] ; ofs +=1 return octet def getbunch(nbr) : global ofs, data bunch =data[ofs:ofs +nbr] ; ofs +=nbr return bunch magic =word(img[0:2]) if magic !=1: input("not a GEM img ! Strike a key") ; sys.exit() plans =word(img[4:6]) ; print("plans: %s" %plans) if plans !=1: input("not a monochrom img ! Strike a key") ; sys.exit() entete =word(img[2:4]) ; print("header: %s" %entete) clusters =word(img[6:8]) ; print("clusters: %s" %clusters) # number of repeated bytes # [8:12] pixel dimensions (screen resolution, 72 dpi?) largeur =word(img[12:14]) ; print("width: %s" %largeur) hauteur =word(img[14:16]) ; print("height: %s" %hauteur) larg, reste =divmod(largeur, 8) if reste : # byte number by ligne (>= line width) larg +1 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 : # multiplication: 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 +=getbunch(octet) elif octet < 128: # ligne +=[0] *octet else: 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 +=[bit] *3 new +=newline *nbl nbl=1 ; ligne =[] header =[ord(x) for x in "P6 %d %d 7\n" %(largeur, hauteur *plans)] with open(nom +".pnm", "wb") as fd : fd.write(bytes(header +new)) try : os.system("convert %s.pnm %s.png" %(nom, nom)) os.system("rm %s.pnm" %nom) input("\n %s.pnm converted into %s.png\n" %(nom, nom)) except : input("\n no .png conversion ; on UNIX, IMageMagic 'convert' should be present\n")
Script pour transformer un fichier IMG 16 couleurs en PNM
Script (python3) à améliorer : certaines images ne passent pas. 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/python3 # From GEM IMG Atari color image to PNM / PNG # Jean-Christophe Beumier - 2015.02.18 - GPLv3 # Translated into python3 - 2023.04.14 - GPLv3 # https://www.gnu.org/licenses/gpl-3.0.en.html # Not completely functional ! 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 =input("numéro de fichier à transformer: ") nom =repertoire[int(q)] print(nom) ofs =0 with open(nom, "rb") as fd : img =list(fd.read()) def word(x): # transform a two characters string into a 16bits word (up to 65535) return x[0] *256 +x[1] def getcar(n): global data, ofs ofs +=1 return data[n] def getbunch(nbr): global ofs, data bunch =data[ofs:ofs +nbr] ; ofs +=nbr return bunch magic =word(img[0:2]) if magic !=1 : print("not a GEM img") ; sys.exit() plans =word(img[4:6]) ; print("plans: %s" %plans) if plans !=4 : print("not a four-planes img") ; sys.exit() entete =word(img[2:4]) ; print("header: %s" %entete) clusters =word(img[6:8]) ; print("clusters: %s" % clusters) # number of repeated bytes # [8:12] pixel dimensions (screen resolution, 72 dpi?) largeur =word(img[12:14]) ; print("width: %s" %largeur) hauteur =word(img[14:16]) ; print("height: %s" %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 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 =word(img[deb +i *6:deb +2 +i *6]) *256 //1000 green =word(img[deb +2 +i*6:deb+ 4+ i *6])* 256 //1000 blue =word(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 line (>= 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: # multiplication: 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 +=getbunch(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 +=[clrs[clr][0], clrs[clr][1], clrs[clr][2]] new +=total *nbl nbl =1 ; newline =[[], [], [], []] plan =(plan +1) %4 header =[ord(x) for x in "P6 %d %d %d\n" %(largeur, hauteur, mx)] with open(nom+".pnm", "wb") as fd : fd.write(bytes(header +new))
↑ PSC PaintShop Compressed
Je ne sais pas si ce format compressé sans perte, uniquement monochrome a eu énormément de succès. Il a été inventé par Thomas Much pour son application PaintShop écrite pour Atari en GFA Basic. La compression ne considère que des lignes, le header fait normalement 14 octets (une extension était prévue) :
- tm89 mot magique pour s'assurer qu'il s"agit bien de ce type de fichier
- xxxx réservé au programme qui a généré le fichier PSC
- 2 octet réservé, toujours de cette valeur
- 1 octet de valeur habituelle 1 ; +1 =2 pour le nombre de mots avant les données
- nn nombre sur deux octets + 1 pour obtenir la largeur en pixels
- nn nombre sur deux octets + 1 pour obtenir la hauteur en pixels
Les données commencent à l'offset 14 (si l'octet 9 =1). Elle sont constituées d'un RLE (run-length encoding). Ce format est très performant si les lignes sont uniformes (même octet ou groupe de deux octets ou si elles se répètent :
- n =0 ligne blanche, pas d'autres octets à prendre
- n =200 ligne noire, pas d'autres octets à prendre
- n =10 prendre un nouvel octet +1 de répétition de la ligne précédente
- n =12 prendre un nouvel octet +256 de répétition de la ligne précédente
- n =100 prendre un nouvel octet à répéter toute la ligne
- n =102 prendre deux nouveaux octets à alterner toute la ligne
- n =110 prendre une ligne d'octets tels quels (ligne non compressée)
- n =255 indique la fin des données
- n =99 données à prendre telles quelles (si le fichier «compressé» est plus long); terminer par 255 (longueur d'un écran =32016)
Ci-dessous, un script en python pour décoder une image PSC. Il n'a pu être testé que pour quatre images seulement. Veuillez m'envoyer toute autre image qui ne se décompressent pas bien.
#! /usr/bin/python3 # https://github.com/thmuch/tosgem-image-reader/blob/main/docs/PaintShopCompressed.md (specs) # https://telparia.com/fileFormatSamples/image/paintShop/ pour quelques images # script python3 ppour transformer une image PaintShop en PNM # Jean-Christophe Beumier - 2023.05.30 - GPLv3 # Aucune garantie - utilisation sous votre propres responsabilité import sys, os liste =os.listdir(".") lon =len(liste) # filtering for i in range(lon-1, -1, -1): if liste[i][-4:].upper() not in (".PSC"): liste.pop(i) liste.sort() # showing directory TNY and consort lon =len(liste) for i in range(lon): print(f"{i:3d} {liste[i]:<14}", end="") if (i+1) % 4 ==0: print() print() nr =input("\n Number(s), space-separated if several (* for all): ") li =nr.split() ; dic ={} # choix des numéros if "*" not in li : li =[int(x) for x in li] if nr !="*" : dic ={x:liste[x] for x in li} else : dic =dict(zip(range(len(liste)), liste)) for num in dic : # loop for all choosen TNY / TN3 files nom =dic[num] with open(nom, "rb") as han : byt =han.read() if byt[0:4] !=b"tm89" : print("Ce n'est pas un fichier PSC d'Atari PaintShop de Thomas Much") # print(byt[4:8]) signature du logiciel générateur # print(byt[8]) toujours 2 ofs =8 +(int(byt[9]) +1) *2 + 2 larg =int(byt[10])* 256 + int(byt[11]) +1 loct =(larg -1) //8 +1 haut =int(byt[12])* 256 + int(byt[13]) +1 image =bytearray() data =byt[ofs:] lon =len(data) ; i =0 ; y =0 while i < len(data) : n =data[i] if n ==99 : # si textuel, rien de compressé i +=1 ; ajout =data[i:-1] ; i +=len(ajout) image +=ajout continue if n ==0 : # une ligne de blanc ajout =b"\x00" *loct image +=ajout i +=1 continue if n ==200 : # une ligne de noir ajout =b"\xff" *loct ; i +=1 ; image +=ajout continue if n ==10 : # répéter la dernière ligne m +1 fois i +=1 ; m =data[i] +1 ; rajout =ajout *m image +=rajout i +=1 continue if n ==12 : # répéter la dernière ligne m +256 fois i +=1 ; m =data[i] +256 rajout =ajout *m image +=rajout i +=1 continue if n ==100 : # une ligne du même octet i +=1 ; m =data[i] ajout =bytes([m]) *loct image +=ajout i +=1 continue if n ==102 : # une ligne du même mot i +=1 ; m = data[i] i +=1 ; p = data[i] ; ajout =bytes([m, p]) *(loct //2) image +=ajout i +=1 continue if n ==110 : # une ligne non compressée i +=1 ; ajout =data[i:i +loct] ; i +=loct image +=ajout continue if n ==255 : # fin des données i +=1 header =bytes(f"P4 {larg} {haut}\n", "ascii") with open(nom+".pnm", "wb") as han : han.write(header +image) print(f" {nom}.pnm sauvegardé") try : os.system(f"convert {nom}.pnm {nom}.png") os.system(f"rm {nom}.pnm") print(f" {nom}.pnm converted into {nom}.png\n") except : print(" no .png conversion ; on UNIX, IMageMagic 'convert' should be present\n")
↑ 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%L =MEMORY(6 +30 *65) ' reservation de memoire BITBLT 98, 72, 235, 65 TO Adr%L BSAVE "OMBAS301.BBT", Adr%L, 30 *65 +6 ' sauvegarde de la memoire vers le disque
Voici le script python qui transforme le BBT monochrome en fichier image .pnm
#! /usr/bin/python3 # Atari Omikron3.01 BBT image to pnm # Jean-Christophe Beumier - 2015.02.09 - GPLv3 # python3 adaptation: 2023.04.15 - GPLv3 # https://www.gnu.org/licenses/gpl-3.0.en.html import os, sys repertoire =os.listdir(".") # for i in range(len(repertoire) -1, -1, -1): if repertoire[i][-4:].upper() !=(".BBT"): repertoire.pop(i) if len(repertoire) ==0 : input("No BBT file in this directory") ; sys.exit() repertoire =sorted(repertoire) for i in range(len(repertoire)): print("%3d %s" %(i, repertoire[i])) q =input("numéro de fichier à transformer: ") nom =repertoire[int(q)] print(nom) with open(nom, "rb") as fd : img =list(fd.read()) def word(w): return w[0] *256 +w[1] rez =word(img[0:2]) if rez !=2 : input("Not an Omikron3.01 BITBLT High Rez (black / white)") ; sys.exit() larg =word(img[2:4]) ; largo =((larg -1) //16) *2 +2 haut =word(img[4:6]) data =img[6:] print(nom +" - length: %s - height: %s - width: %s px - %s words / line\n" %(len(data), haut, larg, largo)) harvest ="" for j in range(haut): ofs =j *largo for i in range(largo): mot =data[ofs +i] for k in range(7, -1, -1): if i *8 +7 -k < larg: if mot & 2**k: harvest +="1" else: harvest +="0" header ="P1 %s %s\n" %(larg, haut) with open(nom+".pnm", "wb") as fd : fd.write(bytes(header +harvest, encoding="ascii")) input("%s.pnm saved" %nom)
↑ Bloc de bits au format .PAD
Il s'agit d'une image en format non compressé proche du précédent, trouvé avec le logiciel PICWORKS fonctionnant sur l'ATARI ST.
- 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 + ou -), 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.. ..++ 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.-+ F3 3E = xxxx ..xx ..xx xx+- 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/python3 # Atari PAD image to PNM # Jean-Christophe Beumier - 2015.02.09 - GPLv3 # python3 adaptation: 2023.06.01 - GPLv3 # https://www.gnu.org/licenses/gpl-3.0.en.html import sys, os liste =os.listdir(".") lon =len(liste) # filtering for i in range(lon-1, -1, -1): if liste[i].rsplit(".", 1)[-1].upper() not in (".PAD"): liste.pop(i) liste.sort() # showing directory TNY and consort lon =len(liste) for i in range(lon): print(f"{i:3d} {liste[i]:<14}", end="") if (i +1) % 4 ==0 : print() print() if not liste : input("\n No .PAD file in this directory.") ; os.exit() nr =input("\n Number(s), space-separated if several (* for all): ") li =nr.split() ; dic ={} # choix des numéros if "*" not in li : li =[int(x) for x in li] if nr !="*" : dic ={x:liste[x] for x in li} else : dic =dict(zip(range(len(liste)), liste)) for num in dic : nom =dic[num] with open(nom, "rb") as fd : img =list(fd.read()) def word(w): return w[0] *256 +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 +f" - length: {len(data)} - height: {haut}px - width: {larg}px - {largo} words/line") harvest="" for j in range(haut): ofs =j *largo for i in range(largo): mot =data[ofs+i] for k in range(7, -1, -1): if i *8 +7 - k < larg: if mot & 2**k: harvest+="1" else: harvest+="0" header="P1 %s %s\n" %(larg, haut) with open(nom+".pnm","wb") as fd : fd.write(bytes(header +harvest, encoding="ascii")) try : os.system("convert %s.pnm %s.png" %(nom, nom)) os.system("rm %s.pnm" %nom) print(f" {nom}.pnm converted into {nom}.png\n", end="") except : print(" no .png conversion ; on UNIX, IMageMagic 'convert' should be present\n", end="") print()