2026-01-02 20:52:44 +01:00
|
|
|
package fr.iutfbleau.sae.mpif;
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
import fr.iutfbleau.sae.util.BitInputStream;
|
|
|
|
|
import fr.iutfbleau.sae.util.DecodeNode;
|
2026-01-04 18:05:46 +01:00
|
|
|
import java.io.BufferedInputStream;
|
2026-01-05 10:19:59 +01:00
|
|
|
import java.io.File;
|
2026-01-02 20:52:44 +01:00
|
|
|
import java.io.FileInputStream;
|
2026-01-05 10:19:59 +01:00
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.List;
|
2026-01-02 20:52:44 +01:00
|
|
|
import java.util.Map;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public class PIFReader {
|
|
|
|
|
|
|
|
|
|
private int width;
|
|
|
|
|
private int height;
|
|
|
|
|
private int[] lenR;
|
|
|
|
|
private int[] lenG;
|
|
|
|
|
private int[] lenB;
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
/**
|
|
|
|
|
* Lit et décode un fichier PIF.
|
|
|
|
|
*
|
|
|
|
|
* @param filepath chemin du fichier PIF
|
|
|
|
|
* @return l'image RGB décodée
|
|
|
|
|
* @throws Exception si erreur de lecture
|
|
|
|
|
*/
|
|
|
|
|
public RGBImage decodePifFile(File file) throws Exception {
|
|
|
|
|
FileInputStream fis = new FileInputStream(file);
|
|
|
|
|
BufferedInputStream bos = new BufferedInputStream(fis);
|
|
|
|
|
BitInputStream lecteur = new BitInputStream(bos);
|
2026-01-02 20:52:44 +01:00
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
// Je lis l'en-tête et les tables canoniques
|
2026-01-02 20:52:44 +01:00
|
|
|
this.readHeader(lecteur);
|
|
|
|
|
this.readCanonicalTables(lecteur);
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
// Je reconstruis les tables canoniques car dans le fichier on a juste les longueurs en bits
|
2026-01-02 20:52:44 +01:00
|
|
|
Map<String, Integer> canonR = rebuildCanonical(lenR);
|
|
|
|
|
Map<String, Integer> canonG = rebuildCanonical(lenG);
|
|
|
|
|
Map<String, Integer> canonB = rebuildCanonical(lenB);
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
// Je construis les arbres de décodage
|
2026-01-02 20:52:44 +01:00
|
|
|
DecodeNode trieR = buildDecodageTree(canonR);
|
|
|
|
|
DecodeNode trieG = buildDecodageTree(canonG);
|
|
|
|
|
DecodeNode trieB = buildDecodageTree(canonB);
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
// Je décode les pixels
|
2026-01-02 20:52:44 +01:00
|
|
|
RGBImage img = decodePixels(lecteur, trieR, trieG, trieB);
|
|
|
|
|
|
|
|
|
|
lecteur.closeFlux();
|
2026-01-05 10:19:59 +01:00
|
|
|
System.out.println("Fichier PIF lu avec succès : " + width + "x" + height);
|
2026-01-02 20:52:44 +01:00
|
|
|
return img;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lit l'en-tête du fichier PIF (largeur et hauteur sur 16 bits chacune).
|
|
|
|
|
* @throws IOException si erreur de lecture
|
|
|
|
|
*/
|
|
|
|
|
public void readHeader(BitInputStream in) throws IOException {
|
2026-01-04 18:13:25 +01:00
|
|
|
this.width = in.readBits(16);
|
|
|
|
|
this.height = in.readBits(16);
|
2026-01-05 10:19:59 +01:00
|
|
|
System.out.println("Dimensions lues : " + this.width + "x" + this.height);
|
2026-01-02 20:52:44 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-04 18:13:25 +01:00
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
/**
|
2026-01-05 11:58:34 +01:00
|
|
|
* Lit les longueurs des code canoniques pour les trois composantes
|
|
|
|
|
* rouge, vert et bleu. Chaque table contient 256 valeurs sur 5 bits.
|
|
|
|
|
* Ces longueurs permettront de reconstruire les vrais codes plus tard
|
|
|
|
|
*
|
|
|
|
|
* @param in flux binaire d'entrée
|
|
|
|
|
* @throws IOException si erreur de lecture se produit
|
2026-01-05 10:19:59 +01:00
|
|
|
*/
|
|
|
|
|
public void readCanonicalTables(BitInputStream in) throws IOException {
|
|
|
|
|
// Table Rouge
|
|
|
|
|
this.lenR = new int[256];
|
2026-01-04 18:13:25 +01:00
|
|
|
for (int i = 0; i < 256; i++){
|
2026-01-05 10:19:59 +01:00
|
|
|
this.lenR[i] = in.readBits(8);
|
2026-01-04 18:13:25 +01:00
|
|
|
}
|
2026-01-05 10:19:59 +01:00
|
|
|
|
|
|
|
|
// Table Vert
|
|
|
|
|
this.lenG = new int[256];
|
|
|
|
|
for (int i = 0; i < 256; i++){
|
|
|
|
|
this.lenG[i] = in.readBits(8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Table Bleu
|
|
|
|
|
this.lenB = new int[256];
|
|
|
|
|
for (int i = 0; i < 256; i++){
|
|
|
|
|
this.lenB[i] = in.readBits(8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
System.out.println("Tables de longueurs lues");
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-05 11:58:34 +01:00
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
/**
|
2026-01-05 11:58:34 +01:00
|
|
|
* Reconstruit les codes canoniques à partir des longueurs stockées dans le fichier.
|
|
|
|
|
* On trie d'abord les paires (symbole, longueur), puis on génère les codes
|
|
|
|
|
* en appliquant la règle des codes canoniques.
|
|
|
|
|
*
|
|
|
|
|
* @param lengths tableau contenant les longueurs des codes pour 256 symboles
|
|
|
|
|
* @return une table qui associe un code binaire (sous forme de texte) à son symbole
|
2026-01-05 10:19:59 +01:00
|
|
|
*/
|
|
|
|
|
public Map<String, Integer> rebuildCanonical(int[] lengths) {
|
|
|
|
|
// je cree une liste de paires (symbole, longueur)
|
|
|
|
|
List<Map.Entry<Integer, Integer>> entiers = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < lengths.length; i++) {
|
|
|
|
|
if (lengths[i] > 0) {
|
|
|
|
|
entiers.add(new java.util.AbstractMap.SimpleEntry<>(i, lengths[i]));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Je trie par longueur croissante, puis par symbole croissant
|
2026-01-05 11:58:34 +01:00
|
|
|
ComparateurEntreeCanonique comparateur = new ComparateurEntreeCanonique();
|
|
|
|
|
entiers.sort(comparateur);
|
2026-01-05 10:19:59 +01:00
|
|
|
|
|
|
|
|
// je genere les codes canoniques
|
|
|
|
|
Map<String, Integer> codes = new HashMap<>();
|
|
|
|
|
int code = 0;
|
|
|
|
|
int previousLength = 0;
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<Integer, Integer> entry : entiers) {
|
|
|
|
|
int symbol = entry.getKey();
|
|
|
|
|
int length = entry.getValue();
|
|
|
|
|
|
|
|
|
|
// Decalage pour aligner le code sur la longueur courante
|
|
|
|
|
code <<= (length - previousLength);
|
|
|
|
|
|
|
|
|
|
// je convertit ce code en texte binaire
|
|
|
|
|
String codeStr = Integer.toBinaryString(code);
|
|
|
|
|
// On s'assure que la chaîne a la bonne longueur en ajoutant des zéros à gauche si nécessaire
|
|
|
|
|
while (codeStr.length() < length) {
|
|
|
|
|
codeStr = "0" + codeStr;
|
|
|
|
|
}
|
|
|
|
|
// je restocke le code + symbole en inversant car la map est inverse dans l'encodage
|
|
|
|
|
codes.put(codeStr, symbol);
|
|
|
|
|
|
|
|
|
|
code++;
|
|
|
|
|
previousLength = length;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return codes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-05 11:58:34 +01:00
|
|
|
* Construit un arbre de décodage à partir des codes canoniques.
|
|
|
|
|
* Chaque code binaire définit un chemin dans l'arbre jusqu'à une feuille
|
|
|
|
|
* contenant le symbole à décoder.
|
2026-01-05 10:19:59 +01:00
|
|
|
*
|
2026-01-05 11:58:34 +01:00
|
|
|
* @param codes dictionnaire associant le code binaire au symbole
|
2026-01-05 10:19:59 +01:00
|
|
|
* @return la racine de l'arbre de décodage
|
|
|
|
|
*/
|
|
|
|
|
public DecodeNode buildDecodageTree(Map<String,Integer> codes) {
|
|
|
|
|
DecodeNode root = new DecodeNode(); // la racine de larbre
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, Integer> entry : codes.entrySet()) {
|
|
|
|
|
String code = entry.getKey(); // 101011101110 par exemple
|
|
|
|
|
int symbol = entry.getValue(); // 0,1,2,3 etc. par exemple
|
|
|
|
|
|
|
|
|
|
DecodeNode current = root;
|
|
|
|
|
|
|
|
|
|
// je parcours le code bit à bit
|
|
|
|
|
for (int i = 0; i < code.length(); i++) {
|
|
|
|
|
char bit = code.charAt(i);
|
|
|
|
|
|
|
|
|
|
if(i == code.length() - 1) {
|
|
|
|
|
// Dernier bit: je cree une feuille avec la valeur du symbol
|
|
|
|
|
if(bit == '0') {
|
|
|
|
|
current.left = new DecodeNode(null, null, symbol);
|
|
|
|
|
} else {
|
|
|
|
|
current.right = new DecodeNode(null, null, symbol);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Si c'est pas le dernier bit : je creer un node interne
|
|
|
|
|
if(bit == '0') {
|
|
|
|
|
if(current.left == null) {
|
|
|
|
|
current.left = new DecodeNode(); // Node interne
|
|
|
|
|
}
|
|
|
|
|
current = current.left;
|
|
|
|
|
} else {
|
|
|
|
|
if(current.right == null) {
|
|
|
|
|
current.right = new DecodeNode(); // node intern
|
|
|
|
|
}
|
|
|
|
|
current = current.right;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return root;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-05 11:58:34 +01:00
|
|
|
* Décode l'ensemble des pixels de l'image en utilisant les trois arbres
|
|
|
|
|
* correspondant aux composantes rouge, verte et bleue.
|
|
|
|
|
* Chaque symbole est lu en parcourant l'arbre bit par bit.
|
2026-01-05 10:19:59 +01:00
|
|
|
*
|
2026-01-05 11:58:34 +01:00
|
|
|
* @param in flux binaire contenant les données des pixels
|
|
|
|
|
* @param red arbre de décodage pour le rouge
|
|
|
|
|
* @param green arbre pour le vert
|
|
|
|
|
* @param blue arbre pour le bleu
|
|
|
|
|
* @return l'image RGB reconstruite
|
|
|
|
|
* @throws IOException si un symbole est invalide ou si la lecture échoue
|
|
|
|
|
*/
|
2026-01-05 10:19:59 +01:00
|
|
|
public RGBImage decodePixels(BitInputStream in, DecodeNode red, DecodeNode green, DecodeNode blue) throws IOException{
|
|
|
|
|
RGBImage image = new RGBImage(width, height);
|
|
|
|
|
|
|
|
|
|
for(int y = 0; y < height; y++) {
|
|
|
|
|
for(int x = 0; x < width; x++) {
|
|
|
|
|
// je decode chaque composante en parcourant son arbre
|
|
|
|
|
int r = decodeSymbole(in, red);
|
|
|
|
|
int g = decodeSymbole(in, green);
|
|
|
|
|
int b = decodeSymbole(in, blue);
|
|
|
|
|
|
|
|
|
|
// je cree et je place le pixel
|
|
|
|
|
Pixel pixel = new Pixel(r, g, b);
|
|
|
|
|
image.setPixel(x, y, pixel);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-01-05 11:58:34 +01:00
|
|
|
* Décode un seul symbole en parcourant l'arbre tant qu'on n'est pas sur une feuille.
|
|
|
|
|
* Chaque bit lu détermine si on part à gauche ou à droite dans l'arbre.
|
|
|
|
|
*
|
|
|
|
|
* @param in flux binaire source
|
|
|
|
|
* @param root racine de l'arbre de décodage
|
|
|
|
|
* @return la valeur du symbole trouvé
|
|
|
|
|
* @throws IOException si un chemin est invalide ou si la lecture échoue
|
2026-01-05 10:19:59 +01:00
|
|
|
*/
|
2026-01-05 11:58:34 +01:00
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
private int decodeSymbole(BitInputStream in, DecodeNode root) throws IOException {
|
|
|
|
|
DecodeNode current = root;
|
|
|
|
|
|
|
|
|
|
// je parcours l'arbre en suivant les bits du flux jusqu'à atteindre une feuille
|
|
|
|
|
while (!current.isLeaf()) {
|
|
|
|
|
int bit = in.readBit();
|
|
|
|
|
if (bit == 0) {
|
|
|
|
|
current = current.left;
|
|
|
|
|
} else {
|
|
|
|
|
current = current.right;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (current == null) {
|
|
|
|
|
throw new IOException("code invalide: noeud null rencontre");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// si on est arrivé à une feuille, on retourne la valeur
|
|
|
|
|
if (current.value == -1) {
|
|
|
|
|
throw new IOException("Feuille sans valeur assignée");
|
|
|
|
|
}
|
|
|
|
|
return current.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-01-05 11:58:34 +01:00
|
|
|
/**
|
|
|
|
|
* Vérifie si un fichier est un fichier .pif valide.
|
|
|
|
|
* On teste l'existence du fichier, son extension et une taille minimale
|
|
|
|
|
* permettant au moins de contenir l'en-tête et les tables de longueurs.
|
|
|
|
|
*
|
|
|
|
|
* @param f fichier à tester
|
|
|
|
|
* @return true si le fichier semble être un .pif valide, sinon false
|
|
|
|
|
*/
|
2026-01-05 10:19:59 +01:00
|
|
|
public static boolean isPIFFile(File f) {
|
|
|
|
|
if (f == null){
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
if (!f.exists() || !f.isFile()){
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//je verifi l'extension
|
|
|
|
|
String name = f.getName().toLowerCase();
|
|
|
|
|
if (!name.endsWith(".pif")) {
|
|
|
|
|
return false;
|
2026-01-04 18:13:25 +01:00
|
|
|
}
|
|
|
|
|
|
2026-01-05 10:19:59 +01:00
|
|
|
return f.length() >= 772; // taille minimal pour un fichier pif longueur largeur et tables de frequance
|
2026-01-02 20:52:44 +01:00
|
|
|
}
|
|
|
|
|
}
|