Lire les images d’écran ATARI ST : PI1-PI3, PC1-PC3, NEO, copies d’écran, TNY

VOUS disposez peut-être encore des images .PI1, .PI2, .PI3, .PC1, .PC2 et .PC3 du logiciel Degas (Elite) ou les images .NEO (et .PIC?) du logiciel Neochrome, et TNY de l’ATARI ST(E), qu’il est difficile de visionner sur les plateformes actuelles. Des solutions libres et partielles ou freeware existent, et une solution libre et complète ici. Vous trouverez ici de telles images.
2023.05.24 - ajout TN3 / TNY haute résolution
2023.05.25 - ajout TN1 / TNY basse résolution
2023.05.06 - ajout TN2 / TNY moyenne résolution

Un utilitaire tout à fait libre, netpbm, contient trois commandes pour transformer les fichiers .PI1 ou .NEO (basse résolution) et .PI3 (haute) en .ppm (portable pixmap), mais pas la moyenne résolution ni les formats compressés. De plus pi1toppm ne tient pas compte de la palette plus fine (16 niveaux de RVB plutôt que 8) des fichiers produits par les (mega)STE.

Le freeware XnViewPM (Win, Mc et Linux) est censé lire ces trois formats d’image, mais je n’y suis pas parvenu.

La solution libre recoil semble difficilement compilable. Une page html permet de lire beaucoup d’images du monde Atari (et Amiga).

Enfin, la ressource Internet 260ste.appspot.com, qui les images aux formats SPECTRUM512 : SPC, SPS, SPU ; GEM : IMG, XIMG ; DEGAS : PI1, PI2, PI3, PC1, PC2, PC3 ; NEOCHROME : NEO : DESKPIC : GBF

atari-ecran.py est un script écrit en python3 pour transformer les images aux dimensions des trois écrans AtariST(E), souvent avec les extensions .DOO, .RAW, .SCR ou autres (données brutes de 32000 octets, résolution et palette de couleurs non précisées), les .PI1, .PI2, .PI3 (format Degas de 32034 ou 32066 octets pour les trois résolutions, données non compressées), .PC1, .PC2, .PC3 (idem mais compressées, de poids variable), .NEO (neochrome, 20128) et .PIC.

Pour d’autres formats d’images sur Atari ST(E), dont les images compressées .IMG, voir ce fichier.

1. Organisation des fichiers

1.1 Résolution
1.2 Couleurs
1.3 Données et couleurs
1.4 Compression
1.5 Fichiers

2. Transformer un .PI3 ou .PC3 (640/400 N/B)

3. Transformer un .PI1 ou .PC1 (320/200, 16 couleurs)

4. Transformer un .PI2 (640/200, 4 couleurs)

5. Transformer un .TNY

5.1 TN3 (haute résolution N/B)
5.2 TN1 (basse résolution, 16 couleurs)
5.3 TN2 (moyenne résolution, 4 couleurs)

Et finalement, le dernier script (PI2) permet également la transformation des PI1 et PI3.

1. Généralités des fichiers écrans

Les fichiers contenant une copie d’écran complète sur Atari sont le plus souvent organisés de la façon suivante :

Ces fichiers non compressés pour chacune des résolutions de l’Atari ST(E) donnent typiquement des copies d’écrans de 32000 (copie brute d’écran, sans définition de couleurs ni de résolution), 32034 (Degas), 32066 (Degas Elite) ou 32128 octets (Neochrome).

Les formats de l’Atari TT différaient de ceux du ST (basse résolution : 320x480 / 256 couleurs, moyenne : 640x480 / 16 couleurs et 1280x960 en haute résolution monochrome), les données non compressées étant codées avec 153 600 octets.

1.1 La résolution

L’Atari ST fonctionne en trois résolutions. Les deux résolutions en couleurs nécessitent un écran dédié (SC1224/5) ou une télévision couleur si l’on dispose d’un modulateur (interne à partir du STFM, «M» pour modulateur); la haute résolution nécessitent un écran dédié en noir et blanc (SM124/5). En mode émulation sur PC, l’écran habituel suffit. Le premier mot (2 octets) du fichier contient les valeurs 0, 1 ou 2:

On voit que c’est la résolution qui détermine les dimensions de l’image, largeur et hauteur de l’écran.

Les extensions .PC1, .PC2 et .PC3 sont destinées aux formats compressés (moins de 32000 octets de données). Les signatures sont (en hexadécimal) 80 00 pour la basse résolution, 80 01 pour la moyenne et 80 02 pour la haute.

Degas aurait (mais aucune image trouvée pour vérifier) adapté son format pour les résolutions Atari-TT, les fichiers pesant alors 153600 + 34 ou 514 octets) :

Le format Neochrome sépare la signature de deux octets vides 00 00 en début de fichier de la résolution de deux octets : 00 00 (pour la basse résolution). Il existerait une version compressée dont la signature et la résolution sont au format Degas : 80 01 pour la compression d’un fichier en moyenne résolution.

1.2 Couleurs

Afin de garder une uniformité de structure entre les différents formats, Degas, Neochrome et Tiny prévoient 32 octets (16 mots) quel que soit le nombre de couleurs utiles :

Les Atari-ST offrent huit valeurs (de 0 à 7) pour chaque composantes vidéo (rouge, vert et bleu), soit 512 possibilités (83) pour chacune des 4 ou 16 couleurs des basse et moyenne résolutions. Le mot (2 octets) définissant une couleur est structuré de la sorte : -----RRR-VVV-BBB (binaire, le - est indéterminé, R, V et B valent 0 ou 1), par exemple 0777 (hexadécimal) : le rouge, le vert et le bleu sont au maximum, pour donner le blanc.

Les couleurs se succèdent en début de fichier, selon l’ordre hardware (c’est à dire du XBIOS) et non l’ordre VDI (voir ici).

Les Atari-STE ont augmenté la sensibilité à 16 valeurs, de 0 à 15. Les valeurs de 0 à 7 sont interprétées par un STE comme allant de 0 à 14, les valeurs intermédiaires étant indiquée par le bit de gauche (en minuscules) : ----rRRR vVVVbBBB. Il résulte deux conséquences de ce bricolage des temps héroïques :

De ce fait, comment un Atari-STE doit-il interpréter une image .PI1 ou .PI2 produite sur un Atari-ST? S’il ajoute systématiquement la demi-valeur 8 pour que le blanc 0777 devienne 0FFF, le noir 0000 deviendra 0888 (le gris le plus foncé). Il est possible de n’ajouter la valeur intermédiaire qu’à partir de la valeur 4, augmentant alors le contraste entre composantes sombres (0, 1, 2 et 3) et composantes claires (4, 5, 6 et 7) de chaque couleur, la valeur 11 n’existant plus (3+8 est l’intermédiaire entre 3 et 4).

Néanmoins, puisqu’il s’agit ici de convertir des fichiers Atari en fichiers capables d’afficher 16 millions de couleurs, ce problème ne nous concerne pas. Il suffit de préciser dans le header du fichier .ppm produit que la teinte maximale est de 7 ou de 15, les teintes intermédiaires étant automatiquement calculées lors de l’affichage, voir la page de description des fichiers pnm/pam.

1.3 Données non compressées

Pour la haute résolution, les 32000 octets (16000 mots) définissent les pixels un à un. Le mot (hexadécimal) C2A5 valant (en binaire) 1101 0010 1010 0101, les deux premiers pixels auront la valeur de la couleur 1 (noir), le troisième de la couleur 0 (blanc), le quatrième la couleur 1, etc. 40 mots (80 octets) sont nécessaires pour la première ligne, qui compte comme les autres 640 pixels, le 41e codera les 16 premiers pixels de la seconde ligne. C’est pour cela que la première couleur de l’entête est codée 0777 (ST) ou 0FFF (STE ou TT), la seconde 0000, les 14 mots suivants n’ayant aucune importance.

En moyenne résolution, la couleur est déterminée par un codage sur deux bits provenant de bits de même rang de deux mots qui se suivent, le bit du premier mot codant l’unité, le second codant la paire :

01011001 00111010 - premier mot
00111000 11110011 - second mot : un bit vaut 0 ou 2
01233001 22331032 - résultats de la combinaison de bits

Le résultat, de 0 à 3, renvoie à l’une des quatre couleurs du début de fichier.

En basse résolution, la couleur est déterminée par un codage sur quatre bits provenant de bits de même rang de quatre mots qui se suivent, le bit du premier mot codant l’unité, le second codant la paire, le troisième la double paire et le quatrième la quadruple paire :

01010101 01010101 - premier mot : un bit vaut 0 ou 1
00110011 00110011 - second mot : un bit vaut 0 ou 2
00001111 00001111 - troisième mot : un bit vaut 0 ou 4
00000000 11111111 - quatrième mot : un bit vaut 0 ou 8
01234567 89ABCDEF - résultats (hexadécimaux) de la combinaison de bits

Le résultat, de 0 à 15, renvoie à l’une des seize couleurs du début de fichier.

1.4 La compression

La compression des écrans Atari (PC1, PC2, PC3) se fait selon la méthode RLE (Run Length Encoding), assez simple : un octet décide de la façon de considérer un certain nombre d’octets suivants.

Degas et Neochrome

Tout se passe par ligne, soit 80 octets en haute résolution (640 px), 160 octets en moyenne résolution (640 px sur deux plans, soit 2 fois 80 octets) et en basse résolution (320 px sur 4 plans, soit 4 fois 40 octets). Deux cas possibles dans la lecture des données brutes de l’écran :

La compression est efficace en cas de longues séries d’un même octet (couleur ou trame uniformes). Par exemple, la suite d’octets (en hexadécimal) F1 FF 01 EB 62 3D 00 se décompose comme ceci :

Conversion chiffre hexadécimal ⇔ binaire / pixels :

0=0000=....  4=0100=.x..  8=1000=x...  C=1100=xx..
1=0001=...x  5=0101=.x.x  9=1001=x..x  D=1101=xx.x
2=0010=..x.  6=0110=.xx.  A=1010=x.x.  E=1110=xxx.
3=0011=..xx  7=0111=.xxx  B=1011=x.xx  F=1111=xxxx

Notes :

Tiny

Le format .TNY part du même principe de RLE, mais le codage est plus complexe. Le fichier comporte deux plages distinctes : une plage de contrôle et une plage de données. La plage de contrôle est parcourue octet par octet, et la plage de données mot par mot (deux octets). Format du fichier :

 1B - résolution: 0 pour basse, 1 pour moyenne, 2 pour haute, 3 pour une animation par rotation de couleurs
16W - définition des 16, 4 ou deux couleurs (ordre VDI ou hardware?)
 1B - deux quartets pour désigner les couleurs de début et de fin prenant part à la rotation
 1B - bit de poids fort pour la direction de la rotation, les sept autres pour le nombre de VBL d’attente
 1W - nombre de rotations à effectuer
 1W - x = nombre d’octets de contrôle
 1W - y = nombre de mots de données
 xB - plage des octets de contrôle
 yW - plage des mots de données

Récursion dans la plage de contrôle, pour un octet valant n:

Note : pour n =0 (répétition d’un mot) ou n =1 (mots à prendre littéralement), le nombre est censé courir de 128 à 32767 inclus, j’ai supposé qu’un seul octet x1 suffit de 128 à 255 et un autre x2 seulement pour les nombres supérieurs (x1 =1 à 127 et x2 de 0 à 255), ce qui semble fonctionner.

En outre, caractéristique de ce format, les mots décompressés s’alignent de façon verticale en quatre colonnes indépendantes en haute résolution, ces quatre mots sont les quatre plans des couleurs des 16 pixels en basse résolution. Pour pour la moyenne résolution, les deux premiers mots s’assemblent pour les deux plans (4 couleurs) de seize pixels, les deux derniers pour une autre rangée de 16 pixels.

1.5 Les différents fichiers

Copie d’écran

Il existe un format de copie d’écran brute, sans spécification de résolution ni de couleur. Ils sont produits par certains logiciels, comme Doodle, et peuvent avoir pour extension .DOO, .SCR, .RAW, .MUR… Le fait que ces fichiers ne comportent ni résolution ni définition de couleurs fait qu’ils sont utilisés par des logiciels simplistes (Doodle est réputé monochrome, bien qu’il existe des fichiers .DOO en moyenne et basse résolutions) ou qui utilisent les couleurs par défaut du ST. Des jeux définissant leurs propres couleurs peuvent utiliser ce genre d’image en sauvegardant les couleurs dans un fichier joint. Tous ces fichiers comportent exactement 32000 octets, quelle que soit leur résolution :

Formats Degas non compressé

Le format Degas, qui connaît les trois extensions .PI1 (basse résolution), .PI2 (moyenne) et .PI3 (haute), utilise deux octets pour la résolution, 32 octets pour la définition des couleurs, et 32000 octets pour les données non compressées. C’est la résolution qui permet de savoir comment sont entrelacées les information sur les couleurs. Ils pèsent toujours 32034 octets, quelle que soit la résolution.

Le format Degas Elite non compressé ajoute 32 octets en fin de fichier pour donner des informations sur la façon dont les images apparaissent lors d’un diaporama. Elles pèsent toujours 32066

Le format NEOchrome .NEO utilise 4 octets pour la signature et la résolution [basse uniquement?], puis 32 octets pour la définition des couleurs, puis encore 92 octets pour une «animation» et des octets réservés, ce qui donne 128 octets d’entête, avant les 32000 octets de données. Certains fichiers .PIC sont très proches de ce format NEO.

Formats Degas compressés

Il semble que seul Degas Elite compresse les données .PC1 (basse résolution), .PC2 (moyenne) et .PC3 (haute). L’entête ne changeant pas, les données commencent à l’octet 34. Elles terminent 32 octets avant la fin, ce qui est simple à spécifier en python : data=image[34:-32]

Il se trouve parfois des fichiers .NEO compressés (que la version 2.28 reconnaît). L’entête est alors différent, commençant par la signature/résolution en deux octets 80 02, suivie par 32 octets de couleurs, comme pour les fichiers Degas PIx et PCx.

2. Transformer un .PI3 (High Rez) en .pnm

Écrit en python, ce script recense tous les fichers .PI3 du répertoire, permettant de désigner ceux qui doivent être transformés selon leur numéro d’ordre (* pour tous). atari-pi3-py2.py pour les nostalgiques de python2, qui n’est plus supporté depuis le 2020.01.01.

#! /usr/bin/python3
# www.jchr.be - py3 version 2020.01.04 - GPL2
import sys, os

repertoires=os.listdir(".")

# filtering ".PI3" files
for i in range(len(repertoires)-1, -1, -1) : # list reverse parsing
  if repertoires[i][-4:].upper() !=".PI3" : # accepts pi3, PI3, Pi3 and pI3
    repertoires.pop(i) # gets rid of the file (doesn't delete it)

repertoires=sorted(repertoires)
leng=len(repertoires)
for i in range(leng) : # showing all .PI3 files of the directory
  print("%3d %s" %(i, repertoires[i]))

rep=input("(space separated) file number(s) to transform, * for all: ")

files =[]
if rep =="*" :
  files =list(repertoires) # OK
else :
  nblist =rep.split()
  for j in nblist :
    i =int(j)
    if i>leng-1 or i<0:
      print(" Sorry! wrong file number…")
      sys.exit()
    else:
      files +=[repertoires[i]]

for nom in files : # processing each file
  han=open(nom, "rb") # binary file (not simple text)
  octets=bytes(han.read())
  han.close()
  img=list(octets) # we'll work with a list of octets

  if len(img) not in [32034, 32066]:
    print("not a .PI3 format by Degas (Elite) on AtariST(E)")
    sys.exit()

  format0=img[0:2] # magic word must be 2 for high rez P3 Degas image
  colours=img[2:6] # two words, first is colour0, second is colour1
  data=img[34:]    # next 32000 bytes are the data

  if format0!=[0, 2] : # magic number for high-rez atari-ST(E) Degas file
    print("not a .PI3 - Degas 640x400 image 2 colours on AtariST(E)")
    if format0==[0, 1] : # magic number for med-rez atari-ST(E) Degas file
      print("seems to be a .PI2 - Degas 640x200 image 4 colours on AtariST(E)")
    if format0==[0, 0] : # magic number for low-rez atari-ST(E) Degas file
      print("seems to be a .PI1 - Degas 320x200 image 16 colours on AtariST(E)")
    sys.exit()

  clr0=[]
  for i in range(2): # colors into numbers
    clr0 +=[colours[i *2] *256 +colours[i *2 +1]]
  test=0 ; flag=0
  for i in range(2) : # only white and black
    test |= clr0[i] # collecting set bits on 'test'
  header =b"P6 640 400 7\n" # by default ST colour-format (8 levels: 0-7)
  # check if STE colour format bit is set ----1---
  if test & int("888", 16) : # checking any bits 3, 7 or 11 is set
    flag=1 # then STE colour-format (16 levels: 0-15 for RGB)
    header =b"P6 640 400 15\n" # not the best format

  clrs=[] # separating ATARI colour code into R, G and B
  for i in range(2): # for all ATARI colour:
    # ----rRRRvGGGbBBB - RRR for red, GGG for green, BBB for blue ; r, v and b if flag==1
    clr=clr0[i] ; blue =clr & 15 ; clr //=16; green =clr & 15; clr //=16; red =clr & 15
    if flag : # STE color: add semi-value (bit3 =8 as a semi value)
      red *=2 +red >>3; green *=2 +green >>3 ; blue *=2 +blue >>3
    else: red &=7; green &=7; blue &=7
    clrs+=[[red, green, blue]]

  newdata =[] # initializing target bytes list
  for i in range(32000) : # treating data bytes (each bit is a pixel)
    byte =data[i]
    for j in range(7, -1, -1):
      px =(byte & 2 **j)>>j
      newdata +=[clrs[px][0], clrs[px][1], clrs[px][2]]
  han=open(nom +".pnm", "wb")
  han.write(header+bytes(newdata))
  han.close()

  print("%s into %s.pnm (saved)" %(nom, nom), end="")
  try :
    os.system("convert %s.pnm %s.png" %(nom, nom)) # Unix-like only? do
    os.system("rm %s.pnm" %(nom))                  # edit for your own OS!
    print(" ; converted into %s.png" %(nom))
  except :
    print(" -- no '.png' conversion, since 'netpbm' is not installed")

Fichier compressé .PC3, haute résolution

Voici un script qui transforme un .PC3 en image PNM (il faudra introduire le nom du fichier .PC3 à transformer).

#! /usr/bin/python3 # réécrit en python3

nom =input("nom: ")
han =open(nom)
img =han.read()
han.close()

rez =img[:2]
header =img[2:34]
data =img[34:-32]

i =0; nv =""
while i<len(data):
  octet =ord(data[i])
  i +=1
  if octet <128:
    nbr =octet+1
    for j in range(nbr):
      nv +=data[i]; i+=1
  else :
    nbr =257-octet
    nv +=data[i] *nbr; i+=1

han =open(nom+".pnm", "w")
han.write("P4 640 400\n" +nv)
han.close()

3. Transformer un fichier .PI1 en .pnm

Suit le script python pour transformer les fichiers Degas PI1 (Atari) en .pnm (Portable aNy Map).

#! /usr/bin/python # attention: python2!!!
import sys, os

repertoires=os.listdir(".")

# filtering ".PI1" files
for i in range(len(repertoires)-1,-1,-1): # reverse list parsing
  if repertoires[i][-4:].upper()!=".PI1": # pi1, PI1, Pi1 or pI1
    repertoires.pop(i)

repertoires=sorted(repertoires)
leng=len(repertoires)
for i in range(leng): # showing all .PI3 files of the directory
  print "%3d %s" %(i, repertoires[i])

rep=raw_input("(space separated) file number(s) to transform, * for all: ")

files=[]
if rep=="*":
  files=list(repertoires) # OK
else:
  nblist=rep.split()
  for j in nblist:
    i=int(j)
    if i>leng-1 or i<0:
      print " Sorry! wrong file number…"
      sys.exit()
    else:
      files+=[repertoires[i]]

for nom in files: # processing each file
  han=open(nom)
  img=han.read()
  han.close()

  if len(img) not in [32034, 32066, 32128]:
    print "not a .PI1 format by Degas (Elite) on AtariST(E): %d octets" %len(img)
    sys.exit()

  format0=img[0:2] # first word must be 2 for high rez pi3 Degas image
  colours=img[2:34] # two words, first is colour0, second is colour1
  data=img[34:] # next 32000 bytes are the data

  newdata="" # initializing target data string

  if format0!=chr(0)+chr(0): # magic number for high-rez atari-ST(E) Degas file
    print "not a .PI1 - Degas 320x200 image / 16 colours on AtariST(E)"
    if format0==chr(0)+chr(1): # magic number for med-rez atari-ST(E) Degas file
      print "seems to be a .PI2 - Degas 640x200 / image 4 colours on AtariST(E)"
    if format0==chr(0)+chr(2): # magic number for low-rez atari-ST(E) Degas file
      print "seems to be a .PI3 - Degas 640x400 / image 2 colours on AtariST(E)"
    sys.exit()

  test=0; flag=0 # trying to see if STE colour format bits exist
  for i in range(2):
    clr=ord(colours[i*2])*256+ord(colours[i*2+1])
    test |= clr # loading bits on 'test'
  header="P6 320 200 7\n" # by default ST colour-format (8 levels: 0-7)
  if test & int("888",16): # checking any bits 3, 7 or 11 is set
    flag=1 # then STE colour-format (16 levels: 0-15 for RGB)
    header="P6 320 200 15\n"

  clrs=[] # initializing a two colours list
  for i in range(16):
    clr=ord(colours[i*2])*256+ord(colours[i*2+1])
    # ----rRRRvGGGbBBB - RRR for red, GGG for green, BBB for blue
    ch=format(clr,"016b")[4:] # get rid of the first 4 bits
    red=int(ch[1:4],2) # 3bits for a value from 0 to 7
    green=int(ch[5:8],2)
    blue=int(ch[9:],2)
    if flag: # STE color: add semi-value
      red=red*2+int(ch[0]) # intercalar value for STE (levels 0-15)
      green=green*2+int(ch[4])
      blue=blue*2+int(ch[8])
    clrs+=[[red, green, blue]]

  for i in range(4000):
    ofs=i*8
    sh0=ord(data[ofs])*256+ord(data[ofs+1])
    sh1=ord(data[ofs+2])*256+ord(data[ofs+3])
    sh2=ord(data[ofs+4])*256+ord(data[ofs+5])
    sh3=ord(data[ofs+6])*256+ord(data[ofs+7])

    for j in range(16):
      jj=15-j
      px=((sh0 & 2**jj)/2**jj)+((sh1 & 2**jj)/2**jj)*2+((sh2 & 2**jj)/2**jj)*4+((sh3 & 2**jj)/2**jj)*8
      newdata+="%c%c%c"%(clrs[px][0], clrs[px][1], clrs[px][2])

  han=open(nom+".pnm","w")
  han.write(header+newdata)
  han.close()

  print "%s >>> %s.pnm (saved)" %(nom, nom),

  try:
    os.system("convert %s.pnm %s.png" %(nom, nom)) # Unix only! do
    os.system("rm %s.pnm" %(nom))                   # edit for other OS!
    print ">>> converted into %s.png" %(nom)
  except:
    print "- no '.png' conversion, since 'ImageMagick' is not installed or other OS than Unix"

Fichier compressé .PC1, basse résolution

Voici un script qui transforme un .PC1 en image PNM (il faudra introduire le nom du fichier .PC1 à transformer). Attention, quatre plans successifs de 40 octets doivent être produits avant de combiner les bits de chaque plan pour en faire des pixels.

#! /usr/bin/python # attention: python2!!!

nom=raw_input("nom: ")
han=open(nom)
img=han.read()
han.close()

colours=img[2:34]; clrs=[]; test=0; flag=0
for i in range(16):
  clr=ord(colours[i*2])*256+ord(colours[i*2+1])
  clrs+=[clr]
  test |= clr # loading bits on 'test'
if test & int("888",16): # checking any bits 3, 7 or 11 is set
  flag=1 # then STE colour-format (16 levels: 0-15 for RGB)

for i in range(len(clrs)):
  # ----rRRRgGGGbBBB - RRR for red, GGG for green, BBB for blue; r, g, b for STE semi-values
  ch=format(clrs[i],"016b")[4:] # get rid of the first 4 bits
  red=int(ch[1:4],2) # 3bits for a value from 0 to 7
  green=int(ch[5:8],2)
  blue=int(ch[9:],2)
  if flag: # STE color: add semi-value
    red=red*2+int(ch[0]) # intercalar value for STE (levels 0-15)
    green=green*2+int(ch[4])
    blue=blue*2+int(ch[8])
  clrs[i]=[red, green, blue]
print clrs
data=img[34:-32]

i=0; nv=""; plan=0; ligne=["","","",""]
while i<len(data):
  octet=ord(data[i]); i+=1
  if octet<128:
    for j in range(octet+1):
      ligne[plan]+=data[i]; i+=1
  else:
    nbr=257-octet
    ligne[plan]+=data[i]*nbr; i+=1
  if len(ligne[plan])>=39:
    if plan==3:
      for j in range(40):
        oct0=ord(ligne[0][j])
        oct1=ord(ligne[1][j])
        oct2=ord(ligne[2][j])
        oct3=ord(ligne[3][j])
        for k in range(7,-1,-1):
          p=2**k
          qu=((oct0 & p) +(oct1 & p)*2+(oct2 & p)*4+(oct3 & p)*8)>>k
          if qu:
            print qu
          nv=nv+chr(clrs[qu][0])+chr(clrs[qu][1])+chr(clrs[qu][2])
          if qu>0:
            clrs[qu]
      ligne=["","","",""]
    plan=(plan+1)%4

maxc=(1+flag)*7+flag # 15 if flag==1, 7 otherwise

han=open(nom+".pnm","w")
han.write("P6 320 200 %d\n" %(maxc) +nv)
han.close()

4. .PI2 (med rez) vers .pnm

Le script qui suit prend finalement en compte les trois formats pi1, pi2 et pi3, en sacrifiant un peu la lisibilité du script. Les scripts pour les fichiers compressés .PC1 et .PC3 n’ont pas été intégrés; le script pour les fichiers .PC2 devrait pouvoir être facilement produit à partir de celui concernant les .PC1 (deux lignes de 80 octets par ligne de pixels).

#! /usr/bin/python # attention: python2!!!
import sys, os

repertoires=os.listdir(".")

# filtering ".PI1", ".PI2" or ".PI3" files
for i in range(len(repertoires)-1,-1,-1): # reverse list parsing
  if repertoires[i][-4:].upper() not in (".PI1", ".PI2", ".PI3"):
    repertoires.pop(i) # get rid if not p|P, i|I, 1|2|3

repertoires=sorted(repertoires)
leng=len(repertoires)
for i in range(leng): # showing all .PI* files of the directory
  print "%3d %s" %(i, repertoires[i])

rep=raw_input("(space separated) file number(s) to transform, * for all: ")
print

files=[]
if rep=="*":
  files=list(repertoires) # OK
else:
  nblist=rep.split()
  for j in nblist:
    i=int(j)
    if i>leng-1 or i<0:
      print " Sorry! wrong file number…"
      sys.exit()
    else:
      files+=[repertoires[i]]

cpt=0
for nom in files: # processing each file
  han=open(nom)
  img=han.read()
  han.close()

  resolution=img[0:2] # Degas image resolution (not compressed)
  rez=ord(resolution[0])*256+ord(resolution[1])
  ext=nom.rsplit(".",1)[1].upper()

  if len(img) not in [32034, 32066, 32128]:
    print "<%s> is not a Degas file (non compressed) on AtariST(E): %d octets" %(nom, len(img))
    sys.exit()

  extensions=("PI1", "PI2", "PI3")
  if ext!=extensions[rez]:
    print "\n  <%s> file has a %s extension but a %s signature! (first file 'word' is %d)" %(nom, ext, extensions[rez], rez)
    if rez==0 or ext=="PI1":
      print "  0 or <.PI1> means a Degas 320x200 image / 16 colours on AtariST(E)"
    if rez==1 or ext=="PI2": # magic number for med-rez atari-ST(E) Degas file
      print "  1 or <.PI2> means a Degas 640x200 image / 4 colours on AtariST(E)"
    if rez==2 or ext=="PI3": # magic number for low-rez atari-ST(E) Degas file
      print "  2 or <.PI3> means a Degas 640x400 image / 2 colours on AtariST(E)"
    print "  <%s> not processed\n" %(nom)
    continue

  colours=img[2:34] # two words, first is colour0, second is colour1
  clrs=[] # initializing colours list
  test=0; flag=0 # do STE colour extra bits exist?
  nbrclr=2**(2**(2-rez)) # rez 0 -> 16 clrs ; 1 -> 4 ; 2 -> 2

  for i in range(nbrclr):
    clr=ord(colours[i*2])*256+ord(colours[i*2+1])
    clrs+=[clr]
    test |= clr # loading bits on 'test'
  if test & int("888",16): # checking any bits 3, 7 or 11 is set
    flag=1 # then STE colour-format (16 levels: 0-15 for RGB)
  # width and height according to rez ; 7 or 15 according to flag (0 or 1):
  header="P6 %d %d %d\n" %((320,640,640)[rez], (200,400,400)[rez], 7*(1+flag)+flag)

  for i in range(len(clrs)):
    # ----rRRRgGGGbBBB - RRR for red, GGG for green, BBB for blue; r, g, b for STE semi-values
    ch=format(clrs[i],"016b")[4:] # get rid of the first 4 bits
    red=int(ch[1:4],2) # 3bits for a value from 0 to 7
    green=int(ch[5:8],2)
    blue=int(ch[9:],2)
    if flag: # STE color: add semi-value
      red=red*2+int(ch[0]) # intercalar value for STE (levels 0-15)
      green=green*2+int(ch[4])
      blue=blue*2+int(ch[8])
    clrs[i]=[red, green, blue]

  data=img[34:] # next 32000 bytes are the data
  plans=2**(2-rez) # low rez 0 -> 4 ; med 1 -> 2 ; high 2 -> 1
  inter=""; newdata="" # initializing target data string
  for i in range(0,32000,plans*2): # low: pace is 8 bytes ; med: 4 ; high: 2
    sh=[]
    for j in range(plans):
      sh+=[ord(data[i+j*2])*256+ord(data[i+j*2+1])]
    for k in range(15,-1,-1): # from 15 to 0 (right bit is 0th)
      px=0
      for j in range(len(sh)):
        px+=(sh[j] & 2**k)/(2**k)*2**j
      inter+="%c%c%c"%(clrs[px][0], clrs[px][1], clrs[px][2])
    if (i+plans*2)%160==0:
       if rez==1: inter*=2 # 640x200 must be converted into 640x400 (Atari ST med rez double height px)
       newdata+=inter; inter=""

  han=open(nom+".pnm","w")
  han.write(header+newdata)
  han.close()
  print "%s >>> %s.pnm (saved)" %((nom,)*2)

5. Transformer des fichiers TNY, TN3 et TN1

Les fichiers Tiny ont plusieurs extensions, soit TNY, seulement reconnaissables à la signature du premier octet et au nombre de mots définis du 2e au 33e octets :

Certains fichiers TNY portent la résolution dans l’extension : TN3, TN2 et TN1

Comme pour la compression PC1 à PC3 ci-dessus, il s'agit d'un RLE (Run-Length Encoding) mais un peu spécial : deux plages sont définies, une de contrôle et une de données, on joue donc sur deux suites d'octets. Le header comporte donc deux mots  :

octets 33 et 34 pour le nombre d'octets de contrôle
octets 35 et 36 pour le nombre d'octets de données

Les octets de contrôle commencent donc à l'octet 33 et se termine à 33 +longueur de la plage contrôle, les datas sont ce qui reste, et sont à considérer comme des mots (ensembles de deux octets). Pour OC (octet de contrôle) :

Une autre différence avec la compression PC1 / PC3 est que les mots ne se suivent pas en ligne mais en colonnes. Les données constituées sont coupées en quatre pour former quatre colonnes parallèles en haute résolution, deux colonnes en moyenne résolution (intriquées deux à deux pour donner les deux plans et donc les quatre couleurs) et en une seule colonne en basse résolution (quatre plans et donc seize couleurs).

Le fichier tny.py reprend les trois scripts qui suivent pour transformer n'importe quel TNY en PNM et même PNG si ImageMagick est installé sur le système (du moins en UNIX). Il offre la possibilité de doubler la taille des fichiers de basse résolution (de 320x200 vers 640x400).

5.1 TN3 (haute résolution)

Le script commence par relever tous les fichiers TNY ou TN3 qui se trouve dans le répertoire courant, et les affiche numérotés. Il est possible d’en choisir plusieurs, séparés par une espace.

Un fichier PNM est créé pour chaque TN3 ou TNY basse résolution, lisible sur un système UNIX avec netpbm installé. Une conversion est réalisée vers PNG si ImageMagick est installé.

#! /usr/bin/python3

# Transforms Atari TNY / TN3 Hi-rez images to PNM / PNG (ImageMagick needed)
#
# Written by Jean-Christophe Beumier - 2015.12.04 - python3: 2023.05.24
# This program is free software GPLv3. It comes without any warranty.
# You can modify it, and redistribute it, under GPLv3 conditions:
# 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][-4:].upper() not in [".TNY", ".TN3"]:
    liste.pop(i)

liste.sort()      # showing directory TNY & TN3
lon =len(liste)
for i in range(lon):
  print(f"{i:3d} {liste[i]}{' ' *(12 -len(liste[i]))}  ", end="")
  if (i+1) % 5 ==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))

def word(x, y) :
  return x *256 +y

def get2ctr(ctrl, ictr) :
  ch2 =ctrl[ictr:ictr +2]
  return word(ch2[0], ch2[1])

for num in dic :  # loop for all choosen TNY / TN3 files
  nom =dic[num]
  with open(nom, "rb") as han :
    img =han.read()

  rez =img[0]
  if rez !=2 :
    print(f"  {num:3d} {nom} is not a TN3 / TNY hi-rez") ; continue
  ctr =word(img[33], img[34])
  dat =word(img[35], img[36])
  ctrl =img[37:37 +ctr]
  data =img[37 +ctr:]

  ictr =0 ; idat =0 ; octets =b""    # uncompressing
  while ictr < ctr:
    octet =ctrl[ictr] ; ictr +=1
    if octet ==0 : # next ctrl word specifies a data word multiplication
      xtr =ctrl[ictr] ; ictr +=1
      if xtr < 128 :                 # if lesser than 128, // only? not quite sure,
        xtr2 =ctrl[ictr] ; ictr +=1  # needs another one   // but it actually works
        xtr =word(xtr, xtr2)
      octets +=data[idat:idat +2] *xtr ; idat +=2
    elif octet ==1 :          # extra ctrl word specifies a literal string length
      xtr =ctrl[ictr] ; ictr +=1
      if xtr < 128 :                # if lesser than 128, // only? not quite sure,
        xtr2 =ctrl[ictr] ; ictr +=1 # needs another one   // but it actually works
        xtr =word(xtr, xtr2)
      octets +=data[idat:idat +xtr *2] ; idat +=xtr *2
    elif octet < 128 : # ctrl byte specifies a data word multiplication
      octets +=data[idat:idat +2] *octet ; idat +=2
    else: # signed ctrl byte specifies a literal string length
      octets +=data[idat:idat +(256 -octet) *2] ; idat +=(256 -octet) *2

# atari screen is 32000 octets; 640 x 400px  for hi-rez, 8px a byte
# 4 parts of 8000 bytes display 4 parallel columns of 16px/bits, 10 times
  image =bytearray(200000)
  for i in range(0, 8000, 2) :
    x, y =divmod(i *2, 800) # y =rest, 400 2-byte words in a column
    for j in range(4) :
      image[y *40 +j *2 +x *8] =octets[i +j *8000]
      image[y *40 +j *2 +x *8 +1] =octets[i +j *8000 +1]

  header =b"P4 640 400\n"
  with open(nom +".pnm", "wb") as han :
    han.write(header +image)
    print(f"{num:3d} {nom}.pnm saved", end="")
  try :
    os.system(f"convert {nom}.pnm {nom}.png")
    os.system(f"rm {nom}.pnm")
    print(", and converted into PNG file", end="")
  except :
    print(", NOT converted into PNG file", end="")
  print()

5.2 TN1 (basse résolution) 23.05.25

Le script commence par relever tous les fichiers TNY ou TN1 qui se trouve dans le répertoire courant, et les affiche numérotés. Il est possible d’en choisir plusieurs, séparés par une espace. Un avertissement lorsque la couleur dépasse 7 est contourné avec [Enter].

Un fichier PNM est créé pour chaque TN1 ou TNY basse résolution, lisible sur un système UNIX avec netpbm installé. Une conversion est réalisée vers PNG si ImageMagick est installé.

#! /usr/bin/python3

# Transforms Atari TNY / TN1 Lo-rez images to PNM / PNG (ImageMagick needed)
#
# Written by Jean-Christophe Beumier - 2015.12.04 - python3: 2023.05.24
# This program is free software GPLv3. It comes without any warranty.
# You can modify it, and redistribute it, under GPLv3 conditions:
# 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][-4:].upper() not in [".TNY", ".TN1"]:
    liste.pop(i)

liste.sort()      # showing directory TNY & TN3
lon =len(liste)
for i in range(lon):
  print(f"{i:3d} {liste[i]}{' ' *(12 -len(liste[i]))}  ", end="")
  if (i+1) % 5 ==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))

def word(x, y) :
  return x *256 +y

def get2ctr(ctrl, ictr) :
  ch2 =ctrl[ictr:ictr +2]
  return word(ch2[0], ch2[1])

for num in dic :  # loop for all choosen TNY / TN3 files
  nom =dic[num]
  with open(nom, "rb") as han :
    img =han.read()

  rez =img[0]
  if rez !=0 :
    print(f"  {num:3d} {nom} is not a TN1 / TNY low-rez") ; continue
  cols =[] ; colz =[]
  for i in range(16) : # Atari mid-rez palette
    red =img[i *2 +1] & 15 ; grn, blu =divmod(img[i *2 +2], 16)
    cols +=[[red, grn, blu]] ; colz +=[red, grn, blu] ; mx =max(colz) ; ste =""
  if mx ==15 : ste =" (STE colors)" # for enhanced color scale for STE

  ctr =word(img[33], img[34]) # number of control bytes
  dat =word(img[35], img[36]) # number of data bytes
  ctrl =img[37:37 +ctr]       # control data
  data =img[37 +ctr:]         # data data

  ictr =0 ; idat =0 ; octets =b""    # uncompressing
  while ictr < ctr:
    octet =ctrl[ictr] ; ictr +=1
    if octet ==0 : # next ctrl word specifies a data word multiplication
      xtr =ctrl[ictr] ; ictr +=1
      if xtr < 128 :                 # if lesser than 128, // only? not quite sure,
        xtr2 =ctrl[ictr] ; ictr +=1  # needs another one   // but it actually works
        xtr =word(xtr, xtr2)
      octets +=data[idat:idat +2] *xtr ; idat +=2
    elif octet ==1 :          # extra ctrl word specifies a literal string length
      xtr =ctrl[ictr] ; ictr +=1
      if xtr < 128 :                # if lesser than 128, // only? not quite sure,
        xtr2 =ctrl[ictr] ; ictr +=1 # needs another one   // but it actually works
        xtr =word(xtr, xtr2)
      octets +=data[idat:idat +xtr *2] ; idat +=xtr *2
    elif octet < 128 : # ctrl byte specifies a data word multiplication
      octets +=data[idat:idat +2] *octet ; idat +=2
    else: # signed ctrl byte specifies a literal string length
      octets +=data[idat:idat +(256 -octet) *2] ; idat +=(256 -octet) *2

# atari screen is 32000 bytes; 640 x 400px for hi-rez, 8px a byte
# 4 parts of 8000 bytes supply words for 4-color-plans (16 colors).
  image =[[]] *192000
  for i in range(0, 8000, 2) :
    x, y =divmod(i, 400)   # y =rest, 200 two-byte words in a column
    q =[0, 0, 0, 0]
    for j in range(4) :    # getting four parallels words
      q[j] =(octets[i +j *8000]) *256 +octets[i +j *8000 +1]
    mots =[]
    for j in range(16) :   # combining four planes from four two-byte words
      mots =[q[0] %2 +(q[1] %2) *2 +(q[2] %2) *4 +(q[3] %2) *8] +mots
      q[0] //=2 ; q[1] //=2 ; q[2] //=2 ; q[3] //=2
    for j in range(16) :
      #print(f"{i:5d}: {y*160} +{x *8} +{j} ={y *160 +x *8 +j}")
      image[y *160 +x *16 +j] =cols[mots[j]]

  for i in range(len(image)) : # useful for debugging
     if image[i] ==[] : image[i] =[3, 3, 3]
  img =[]
  for i in range(len(image)) :
    img +=image[i]

  header =bytes(f"P6 320 200 {mx}\n", "ascii")
  with open(nom +".pnm", "wb") as han :
    han.write(header +bytearray(img))
    print(f"{num:3d} {nom}.pnm saved{ste}", end="")
  try :
    os.system(f"convert {nom}.pnm {nom}.png")
    os.system(f"rm {nom}.pnm")
    print(", and converted into PNG file", end="")
  except :
    print(", NOT converted into PNG file", end="")
  print()

5.3 TN2 (moyenne résolution) 23.05.25

Le script commence par relever tous les fichiers TNY ou TN2 qui se trouvent dans le répertoire courant, et les affiche numérotés. Il est possible d’en choisir plusieurs, séparés par une espace.

Un fichier PNM est créé pour chaque TN2 ou TNY basse résolution, lisible sur un système UNIX avec netpbm installé. Une conversion est réalisée vers PNG si ImageMagick est installé.

#! /usr/bin/python3

# Transforms Atari TNY / TN2 Mid-rez images to PNM / PNG (ImageMagick needed)
#
# Written by Jean-Christophe Beumier - 2023.05.26
# This program is free software GPLv3. It comes without any warranty.
# You can modify it, and redistribute it, under GPLv3 conditions:
# 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][-4:].upper() not in [".TNY", ".TN2"]:
    liste.pop(i)

liste.sort()      # showing directory TNY & TN2
lon =len(liste)
for i in range(lon):
  print(f"{i:3d} {liste[i]}{' ' *(12 -len(liste[i]))}  ", end="")
  if (i+1) % 5 ==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))

def word(x, y) :
  return x *256 +y

def get2ctr(ctrl, ictr) :
  ch2 =ctrl[ictr:ictr +2]
  return word(ch2[0], ch2[1])

for num in dic :  # loop for all choosen TNY / TN3 files
  nom =dic[num]
  with open(nom, "rb") as han :
    img =han.read()

  rez =img[0]
  if rez !=1 :
    print(f"  {num:3d} {nom} is not a TN2 / TNY mid-rez") ; continue
  cols =[] ; colz =[]
  for i in range(4) : # Atari mid-rez palette
    red =img[i *2 +1] & 15 ; grn, blu =divmod(img[i *2 +2], 16)
    cols +=[[red, grn, blu]] ; colz +=[red, grn, blu] ; mx =max(colz) ; ste =""
  if mx ==15 : ste ="(STE colors)" # for enhanced color scale for STE

  ctr =word(img[33], img[34]) # number of control bytes
  dat =word(img[35], img[36]) # number of data bytes
  ctrl =img[37:37 +ctr]       # control data
  data =img[37 +ctr:]         # data data

  ictr =0 ; idat =0 ; octets =b""    # uncompressing
  while ictr < ctr:
    octet =ctrl[ictr] ; ictr +=1
    if octet ==0 : # next ctrl word specifies a data word multiplication
      xtr =ctrl[ictr] ; ictr +=1
      if xtr < 128 :                 # if lesser than 128, // only? not quite sure,
        xtr2 =ctrl[ictr] ; ictr +=1  # needs another one   // but it actually works
        xtr =word(xtr, xtr2)
      octets +=data[idat:idat +2] *xtr ; idat +=2
    elif octet ==1 :          # extra ctrl word specifies a literal string length
      xtr =ctrl[ictr] ; ictr +=1
      if xtr < 128 :                # if lesser than 128, // only? not quite sure,
        xtr2 =ctrl[ictr] ; ictr +=1 # needs another one   // but it actually works
        xtr =word(xtr, xtr2)
      octets +=data[idat:idat +xtr *2] ; idat +=xtr *2
    elif octet < 128 : # ctrl byte specifies a data word multiplication
      octets +=data[idat:idat +2] *octet ; idat +=2
    else: # signed ctrl byte specifies a literal string length
      octets +=data[idat:idat +(256 -octet) *2] ; idat +=(256 -octet) *2

# atari screen is 32000 bytes; 640 x 400px for hi-rez, 8px a byte
# 4 parts of 8000 bytes display 4 parallel columns of 16px/bits, 10 times
  image =[[]] *256000
  for i in range(0, 8000, 2) :
    x, y =divmod(i, 400)   # y =rest, 200 two-byte words in a column
    q =[0, 0, 0, 0]
    for j in range(4) :    # getting four parallels words
      q[j] =(octets[i +j *8000]) *256 +octets[i +j *8000 +1]
    mots1 =[] ; mots2 =[]
    for j in range(16) :   # combining two by two planes from four two-byte words
      mots1 =[q[0] %2 +(q[1] %2) *2] +mots1
      mots2 =[q[2] %2 +(q[3] %2) *2] +mots2
      q[0] //=2 ; q[1] //=2 ; q[2] //=2 ; q[3] //=2
    for j in range(16) :
      #print(f"{i:5d}: {y*160} +{x *32} +{j} ={y *160 +x *32 +j}")
      image[y *640 +x *32 +j] =cols[mots1[j]]
      image[(y +1) *640 +x *32 +j] =cols[mots1[j]]
      image[y *640 +x *32 + 16 +j] =cols[mots2[j]]
      image[(y +1) *640 +x *32 + 16 +j] =cols[mots2[j]]

  img =[]
  for i in range(len(image)) :
    img +=image[i]

  header =bytes(f"P6 640 400 {mx}\n", "ascii")
  with open(nom +".pnm", "wb") as han :
    han.write(header +bytearray(img))
    print(f"{num:3d} {nom}.pnm saved {ste}", end ="")
  try :
    os.system(f"convert {nom}.pnm {nom}.png")
    os.system(f"rm {nom}.pnm")
    print(", and converted into PNG file", end ="")
  except :
    print(", NOT converted into PNG file", end ="")
  print()