Préalable

Le rendu des TP se fera via GitHub ou GitLab. Avant de commencer les TP/TD, vous devrez donc impérativement suivre les étapes décrites en introduction de ce cours. Même si ce TD n’est, en principe, pas noté, la configuration initiale fait partie du travail demandé. Afin de préparer la mise en place des TP suivants et de ne pas perdre de temps. Normalement vous avez déjà vue cela, donc pas d’excuse. Une fois votre dépôt créé et avant d’ajouter le répertoire pour le premier TP, créez un fichier .gitignore à la racine de votre dépôt contenant *.class et *.jar.

echo "*.class" >> .gitignore
echo "*.jar" >> .gitignore
echo "*.o" >> .gitignore
echo "*.obj" >> .gitignore

Ce fichier .gitignore doit donc se trouver à la racine de votre dépôt, valable pour tous les TPs. Une fois les dépôts Git (local et distant) créés, vous pouvez passer à la suite :

Le répertoire de ce TP dans votre dépôt s’appellera “LectureEcritureJava”.

mkdir LectureEcritureJava

Créez ensuite un fichier .gitignore dans ce répertoire, contenant le nom des fichiers compilés et exécutables.

Organisation attendu

OS_Nom1_Nom2/
├─ .gitignore                # général (*.class, *.jar, etc.)
├─ README.md                 # informations additionelles
└─ LectureEcritureJava/
   ├─ .gitignore             # spécifique à ce TP
   ├─ Image.java
   ├─ PPM.java
   ├─ FirstPPM.java
   ├─ Gradient.java
   ├─ Darker.java
   └─ Main.java

Ce qui n’est pas poussé sur votre dépôt GitLab n’est pas corrigé. Pensez donc bien à effectuer des commit et des push régulièrement. Afin de pratiquer les entrées-sorties en Java, vous allez manipuler des images au format PPM, un grand classique.

Attention, tous travail est suseptible d’être noté y compris ce TD. Particulièrement en cas de manque de motivation, de non soumission (0) ou d’utilisation d’IA (0). Si des éléments sont manquants, n’hésitez pas à faire remonter l’information. Je peux également vérifier les commits. Tout commit en dehors des heures de cours est proscrit.

Création d’une image manuelle

La première étape va consister à créer une image en écrivant dans un fichier. Vous allez travailler avec le format PPM qui permet de stocker simplement les valeurs des différents pixels directement sous forme matricielle. Par exemple, pour encoder l’image avec 6 pixels suivante :

Figure 1 : FirstPPM.ppm

Un fichier PPM se compose de deux parties : un en-tête et les données d’image. L’en-tête se compose d’au moins trois parties, généralement délimitées par des retours chariot et/ou des sauts de ligne, mais la spécification PPM n’exige que des espaces blancs. Il faut respecter les etapes suivantes :

  • “P3” pour le format ASCII, RGB. (On parle aussi d’identifiant magique)
  • 3 pixels de large, 2 pixels de haut
  • Valeur maximale possible pour les composants (rouge, vert ou bleu)
  • Les valeurs des composantes sont ensuite mises les unes à la suite des autres (R G B R G B …)
  • Chaque ligne de pixel donne une ligne dans le fichier PPM

En plus des lignes obligatoires ci-dessus, un commentaire peut être placé n’importe où à l’aide du caractère #. Le commentaire s’étend jusqu’à la fin de la ligne, comme en Python. Il devra être pris en compte plus tard afin d’omettre le commentaire lors du traitement de l’image.

Donnera donc le fichier PPM :

P3
3 2
255
255 0 0 0 255 0 0 0 255
255 255 0 255 255 255 0 0 0

Ouverture et écriture dans un fichier

Écrire dans un fichier est une opération fondamentale dans la plupart des langages de programmation, permettant de stocker des données de manière persistante ou de générer des fichiers destinés à d’autres programmes. Selon le langage et le niveau d’abstraction choisi, cette tâche peut se faire directement via des descripteurs de fichiers bas niveau, offrant un contrôle précis et rapide avec le noyau, ou via des flux/objets plus haut niveau qui simplifient la gestion des tampons et des erreurs. La compréhension des mécanismes sous-jacents, comme le concept de File Descriptor en C, est essentielle pour écrire du code robuste et éviter les fuites de ressources ou les comportements inattendus.

En C (version linux):

#include <fcntl.h>     // For 'open'
#include <unistd.h>    // For 'write'
#include <string.h>    // For 'strlen'
#include <stdio.h>     // For 'snprintf'
#include <stdlib.h>
#include <errno.h>

int main(int argc, char *argv[]) {
    const char *en_tete = "P3\n100 100\n255\n";

    // Création du fichier : Ecrasement si existe, lecture/écriture, droits rw- pour l'utilisateur
    int fd = open("firstPPM.ppm", O_CREAT | O_TRUNC | O_RDWR, S_IRUSR | S_IWUSR);

    if (fd == -1) {
        perror("Erreur à l'ouverture du fichier");
        return EXIT_FAILURE;
    }

    ssize_t bytes_written = write(fd, en_tete, strlen(en_tete));

    if (bytes_written == -1) {
        perror("Erreur à l'écriture dans le fichier");
        close(fd);  // On ferme quand même le fichier
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Ici, fd signifie File Descriptor : c’est un entier représentant une ressource ouverte par le système (fichier, socket, etc.). Le programmeur doit gérer manuellement l’ouverture, l’écriture et la fermeture. Le système d’exploitation garde la trace de tous les descripteurs de fichiers ouverts par chaque programme. L’entier fd correspond à un index dans une table de mappage interne gérée par le noyau. On peut observer cette table depuis l’utilisateur via /proc/self/fd (self etant le processus courant) :

$ ls -id -l /proc/self/fd
total 0
267056 lrwx------. 1 ovan ovan 64 Jan 22 19:28 0 -> /dev/pts/0
267057 lrwx------. 1 ovan ovan 64 Jan 22 19:28 1 -> /dev/pts/0
267058 lrwx------. 1 ovan ovan 64 Jan 22 19:28 2 -> /dev/pts/0
270772 lr-x------. 1 ovan ovan 64 Jan 22 19:28 3 -> /proc/2645/fd

Ici, ce sont les descripteurs de fichiers ouverts par ls. À chaque exécution, le PID de ls change (c’est une nouvelle instance du programme) et de nouveaux fd sont générés. Les trois premiers id sont des flux standards préexistants, souvent appelés « fichiers virtuels », utilisés pour transmettre des données entre le programme et l’environnement. Les descripteur de fichier sont les suivants :

  • (0) stdin : le flux d’entrée standard, généralement le clavier ou l’entrée d’un autre programme.
  • (1) stdout : le flux de sortie standard, utilisé pour afficher des informations à l’écran ou les rediriger vers un fichier.
  • (2) stderr : le flux de sortie d’erreurs, séparé de stdout pour permettre de distinguer les messages d’information des messages d’erreur.
  • (3) correspond probablement a l’ouverture du dossier /proc/self/fd, c’est donc en realiter un inode

Dans le tableau précédent, le premier chiffre correspond au numéro d’inode. Par exemple, si Firefox a le PID 6073, alors le répertoire /proc/6073 existe. Depuis ce processus, /proc/self est un lien symbolique vers /proc/6073, ce qui facilite l’accès aux informations du processus courant. Dit autrement c’est un pointeur.

Attention : /proc est un dossier virtuel créé par le noyau. Il ne contient pas de fichiers physiques sur le disque, mais expose des informations sur les processus et le système en temps réel. C’est un pseudo-fs.

ls -la /proc/6073/fd
total 0
dr-x------ 2 ovan ovan 460 Sep  4 11:29 .
dr-xr-xr-x 9 ovan ovan   0 Sep  4 11:29 ..
lr-x------ 1 ovan ovan  64 Sep  8 18:56 0 -> /dev/null
l-wx------ 1 ovan ovan  64 Sep  8 18:56 1 -> /dev/null
lrwx------ 1 ovan ovan  64 Sep  8 18:56 10 -> 'anon_inode:[eventfd]'
lrwx------ 1 ovan ovan  64 Sep  8 18:56 100 -> 'anon_inode:[eventfd]'
lrwx------ 1 ovan ovan  64 Sep  8 18:56 101 -> 'anon_inode:[eventpoll]'
lrwx------ 1 ovan ovan  64 Sep  8 18:56 102 -> 'socket:[31432]'
lrwx------ 1 ovan ovan  64 Sep  8 18:56 103 -> 'socket:[32402]'
l-wx------ 1 ovan ovan  64 Sep  8 18:56 107 -> 'pipe:[32426]'
lrwx------ 1 ovan ovan  64 Sep  8 18:56 114 -> bounce-tracking-protection.sqlite
lrwx------ 1 ovan ovan  64 Sep  8 18:56 115 -> content-prefs.sqlite
lrwx------ 1 ovan ovan  64 Sep  8 18:56 16 -> /dev/nvidiactl
lrwx------ 1 ovan ovan  64 Sep  8 18:56 263 -> '/memfd:pulseaudio'
lrwx------ 1 ovan ovan  64 Sep  8 18:56 319 -> /dev/nvidia-modeset
lrwx------ 1 ovan ovan  64 Sep  8 18:56 45 -> /dev/nvidia0
...

De cette façon, on se rend compte que chaque entier fd correspond à une ressource système unique, et que le noyau maintient la correspondance entre cet entier et l’objet réel (fichier, socket, pipe, etc.). Cela permet également de comprendre pourquoi oublier de fermer un fd peut bloquer des ressources sur le système jusqu’à la libération de cette derniere. Dans cet exemple, on retrouve :

  • les entrées/sorties usuelles
  • des connexions réseaux
  • des communications inter-processus (IPC : pipe)
  • des fichiers
  • le serveur d’affichage via NVIDIA
  • le service audio via PulseAudio

Supposons que le programme dialogue directement avec une imprimante (en réalité, c’est le service CUPS qui gère l’impression). Si le programme oublie de fermer la ressource et continue de tourner, il pourrait bloquer l’accès à l’imprimante pour tout autre programme, empêchant ainsi toute impression tant que le processus n’a pas libéré le descripteur de fichier.

En résumé : le File Descriptor est le lien direct entre le programme et le noyau, et observer /proc/[pid]/fd permet de visualiser concrètement toutes les ressources ouvertes par un processus à un instant donné.C’est la table des descripteurs ouverts. Au niveau du système, il existe une table des fichiers ouverts partagée par tous les processus. Chaque entrée de cette table contient des informations sur l’état du fichier (position du curseur, indicateurs d’erreur et de fin de fichier), ainsi qu’un pointeur vers un i-node (c’est-à-dire sa localisation physique ou virtuelle).

flowchart TD subgraph Processus1["Premier Processus"] P1[Programme en cours d'exécution] T1[Table des descripteurs de fichiers] subgraph FDs1["Descripteurs Standards"] STDIN1["stdin (fd 0)"] STDOUT1["stdout (fd 1)"] STDERR1["stderr (fd 2)"] end P1 --> T1 T1 --> STDIN1 T1 --> STDOUT1 T1 --> STDERR1 end subgraph Processus2["Second Processus"] P2[Programme en cours d'exécution] T2[Table des descripteurs de fichiers] subgraph FDs2["Descripteurs Standards"] STDIN2["stdin (fd 0)"] STDOUT2["stdout (fd 1)"] STDERR2["stderr (fd 2)"] end P2 --> T2 T2 --> STDIN2 T2 --> STDOUT2 T2 --> STDERR2 end subgraph Systeme["Niveau Système"] subgraph Inodes["Informations Physiques"] I1["inode 1:<br/>• Position du curseur<br/>• Indicateurs d'erreur<br/>• Pointeur inode"] I2["inode 2:<br/>• Position du curseur<br/>• Indicateurs d'erreur<br/>• Pointeur inode"] I3["inode 3:<br/>• Position du curseur<br/>• Indicateurs d'erreur<br/>• Pointeur inode"] I4["inode 4:<br/>• Position du curseur<br/>• Indicateurs d'erreur<br/>• Pointeur inode"] I5["inode 5:<br/>• Position du curseur<br/>• Indicateurs d'erreur<br/>• Pointeur inode"] I6["inode 6:<br/>• Position du curseur<br/>• Indicateurs d'erreur<br/>• Pointeur inode"] end end STDIN1 --> I1 STDOUT1 --> I2 STDERR1 --> I3 STDIN2 --> I4 STDOUT2 --> I5 STDERR2 --> I6 classDef process fill:#a8d08d,stroke:#507e32,color:#333 classDef desc fill:#95b3d7,stroke:#355e8c,color:#333 classDef sys fill:#e6b8b7,stroke:#953735,color:#333 classDef inode fill:#ffe599,stroke:#b08917,color:#333 class P1,T1,P2,T2 process class STDIN1,STDOUT1,STDERR1,STDIN2,STDOUT2,STDERR2 desc class TF sys class I1,I2,I3,I4,I5,I6 inode

Modes d’ouvertures :
- Lecture seule (O_RDONLY)
- Écriture seule (O_WRONLY)
- Lecture-Écriture (O_RDWR)
- O_APPEND
- O_CLOEXEC
- …
- O_CREAT : création du fichier s’il n’existe pas
- O_DIRECTORY : erreur si le nom n’est pas un répertoire
- O_EXCL : assure la création du fichier (erreur sur fichier existant)
- O_TRUNC : efface le contenu du fichier après ouverture
- …
- S_IRWXU, S_IRUSR, S_IWUSR, S_IXUSR → 0700, 0400, 0200, 0100, → rwx – –, r– – –, -w- – –, –x – –
- S_IRWXG, S_IRGRP, S_IWGRP, S_IXGRP → 0070, 0040, 0020, 0010, → – rwx –, – r– –, – -w- –, – –x –
- S_IRWXO, S_IROTH, S_IWOTH, S_IXOTH → 0007, 0004, 0002, 0001, → – – rwx, – – r–, – – -w-, – – –x

En C (version lib standard):

#include <stdio.h>

int main (int argc, char *argv[]) {
    FILE *fp = fopen("firstPPM.ppm", "w"); // ouverture en écriture (création/écrasement)

    if (fp == NULL) {
        perror("Erreur d'ouverture du fichier");
        return 1;
    }

    const char *en_tete = "P3\n100 100\n255\n";
    fprintf(fp, "%s", en_tete); // écriture du texte dans le fichier

    fclose(fp); // fermeture explicite du fichier
    return 0;
}

Ici, on utilise un flux FILE* plutôt qu’un descripteur de fichier brut. La bibliothèque standard C ajoute une couche d’abstraction. Un tampon mémoire est utilisé pour optimiser les écritures, les fonctions (fprintf, fscanf, etc.) facilitent la manipulation de données textuelles

  • r Ouvre le fichier en lecture, curseur positionné au début
  • r+ Ouvre le fichier en lecture et écriture, curseur positionné au début
  • w Efface ou le crée le fichier, l’ouvre en écriture seule, curseur positionné au début
  • w+ Idem mais ouverture en lecture-écriture
  • a Ouvre ou crée le fichier en écriture seule, curseur à la fin du fichier.
  • a+ Ouvre ou crée le fichier en lecture-écriture, curseur d’écriture à la fin du fichier, curseur de lecture au début.

En Java: En Java, l’écriture de fichiers se fait principalement avec les classes FileWriter, BufferedWriter, ou FileOutputStream ou encore RandomAccessFile. Java gère automatiquement beaucoup d’aspects comme l’ouverture et la fermeture des fichiers. La gestion des erreurs ce fais via les exceptions et reste toujours à la charge du programmeur (cette pratique, a la base de Java, entraîne un coût supplémentaire important). L’accès direct au descripteur reste parfois possible via getFD() suivant le niveau d’abstraction utilisé, mais son usage reste rare car la JVM masque la plupart des détails système. (Notez que le C++ ou Rust dispose aussi de ces avantages, sans les inconvegnients).

import java.io.FileOutputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        String enTete = "P3\n100 100\n255\n";

        try {
            FileOutputStream fos = new FileOutputStream("firstPPM.ppm");
            fos.write(enTete.getBytes()); // écriture directe des octets
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.FileWriter;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        String enTete = "P3\n100 100\n255\n";

        try {
            FileWriter fw = new FileWriter("firstPPM_withFileWriter.ppm");
            fw.write(enTete); // écriture du texte directement
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.IOException;
import java.io.RandomAccessFile;

public class Main {
    public static void main(String[] args) {
        String enTete = "P3\n100 100\n255\n";

        try {
            RandomAccessFile raf = new RandomAccessFile("firstPPM_withRAF.ppm", "rw");
            raf.writeBytes(enTete.getBytes()); // écrit le texte en octets
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

En Java : La classe System contient 3 references in, out et err correspondent à des flux (streams) connectés aux descripteurs de fichiers (FileDescriptor) ouverts par le système.

Première image : FirstPPM.java

Créez et ajoutez à Git un fichier Java nommé FirstPPM.java. Puis, en partant du code ci-dessous, créez et écrivez dans un fichier nommé “FirstPPM.ppm” pour reproduire la Figure 1. Une fois le code compilé et executé, vous pouvez ouvrir le fichier produit, FirstPPM.ppm, avec un éditeur de texte pour en examiner le contenu et verifier que ce qui est obtenu correspond à ce qui est attendu.

import java.io.FileWriter;
import java.io.IOException;

public class FirstPPM {
    public static void main(String[] args) {
        try {
            FileWriter writer = new FileWriter("FirstPPM.ppm");

            writer.write("P3\n");
            // Écriture des dimensions
            // Écriture de la valeur maximal
            // Écriture des pixels
            // Première ligne : rouge, vert, bleu
            // Deuxième ligne : jaune, blanc, noir

            writer.close(); // Fermeture du fichier

            System.out.println("Image PPM créée avec succès !");
        } catch (IOException e) {
            System.err.println("Erreur lors de l'écriture du fichier : " + e.getMessage());
        }
    }
}

Création d’un dégradé

L’objectif est de produire un dégradé de bleu d’au moins 200x100 pixels :

Pour ce faire nous allons creer une abstraction de la notion d’image. Créez un fichier Image.java. Cette classe seras utile pour la suite du TP/TD.

import java.io.FileWriter;
import java.io.IOException;

public class Image {
    private int width;
    private int height;
    // pixels[y][x][0=R,1=G,2=B]
    private int[][][] pixels; // pixels[y][x][0=R,1=G,2=B]

    public int getWidth() { return width; }
    public int getHeight() { return height; }

    /**
     * Constructeur : initialise une image vide.
     */
    public Image(int width, int hauteur) {
        this.width = width;
        this.height = height;
        pixels = new int[hauteur][largeur][3];
    }

    /**
     * Définit la couleur d'un pixel à la position (x, y)
     */
    public void setPixel(int x, int y, int r, int g, int b) {
        if (x >= 0 && x < largeur && y >= 0 && y < hauteur) {
            pixels[y][x][0] = r;
            pixels[y][x][1] = g;
            pixels[y][x][2] = b;
        }
    }

    /**
     * Sauvegarde l'image au format texte PPM (P3)
     */
    public void save_txt(String filename) throws IOException {
        // TODO : écrire le fichier PPM avec FileWriter
    }
}

Pourquoi des int ont étés utilisés pour stocker les pixels ? Que peut on faire de mieux ?

Enfin créez et completez le fichier Gradient.java.

public class Gradient {
    public static void main(String[] args) {
        Image img = new Image(200, 100);

        // Génération du dégradé de bleu
        for (int y = 0; y < hauteur; y++) {
            for (int x = 0; x < largeur; x++) {
                int bleu = 0; // quel calcule ?
                img.setPixel(x, y, 0, 0, bleu);
            }
        }

        try {
            img.save("gradient.ppm");
            System.out.println("Dégradé créé avec succès !");
        } catch (Exception e) {
            System.err.println("Erreur lors de la création du dégradé : " + e.getMessage());
        }
    }
}

Pour observer vos images, vous utiliserez un logiciel d’affichage d’image (gimp, eog, gwenview, paint …) et non plus un éditeur de texte.

Lire depuis un fichier

Lire un fichier est une opération complémentaire de la l’écriture, qui permet de récupérer des données stockées sur le disque ou générées par un autre programme. Voici un exemple en C utilisant un descripteur de fichier pour lire les premiers octets d’un fichier PPM :

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main() {
    char buffer[128];
    int fd = open("firstPPM.ppm", O_RDONLY);

    if (fd == -1) {
        perror("Erreur à l'ouverture du fichier");
        return EXIT_FAILURE;
    }

    ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);

    if (bytes_read == -1) {
        perror("Erreur à la lecture du fichier");
        close(fd);
        return EXIT_FAILURE;
    }

    buffer[bytes_read] = '\0';
    printf("%s\n", buffer);

    // ici buffer peut contenir plusieurs lignes

    close(fd);
    return EXIT_SUCCESS;
}

Lorsqu’on utilise la bibliothèque standard C, la lecture devient plus simple grâce aux flux. Le code suivant montre comment lire la première ligne d’un fichier PPM avec FILE* :

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp = fopen("firstPPM.ppm", "r");

    if (fp == NULL) {
        perror("Erreur d'ouverture du fichier");
        return 1;
    }

    char buffer[128];
    if (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    fclose(fp);
    return 0;
}

Ici, FILE* représente un flux avec tampon intégré. Les fonctions comme fgets ou fread permettent de manipuler facilement le contenu d’un fichier tout en profitant de la gestion “automatique” des tampons. Il exist egalement la fonction getline bien pratique.

En Java, l’écriture et la lecture de fichiers sont gérées par des classes comme FileInputStream, BufferedReader ou RandomAccessFile. Ces classes masquent la majorité des détails liés aux descripteurs système, mais la logique reste la même : ouvrir le fichier, lire ou écrire, puis fermer correctement la ressource. La gestion des erreurs se fait toujours via des exceptions. L’exemple suivant montre la lecture des premiers octets d’un fichier PPM en Java :

import java.io.FileInputStream;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        try {
            FileInputStream fis = new FileInputStream("firstPPM.ppm")
            byte[] buffer = new byte[128];
            int bytesRead = fis.read(buffer);
            System.out.println(new String(buffer, 0, bytesRead));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Dans cet exemple, FileInputStream lit directement des octets, ce qui le rend approprié pour des fichiers binaires comme les ppm P6. Si l’on travaille avec des fichiers texte, on peut préférer BufferedReader ou RandomAccessFile pour lire ligne par ligne ou accéder à des positions spécifiques dans le fichier.

Ainsi, la lecture depuis un fichier, qu’elle soit effectuée en C ou en Java, repose toujours sur les mêmes principes : établir un lien avec le système via un descripteur ou un flux, accéder aux données, puis libérer correctement la ressource. La connaissance de ces mécanismes permet de mieux comprendre le fonctionnement interne des programmes.

Modification d’une image

Ajoutez la fonction static read_txt a la classe Image :

static public read_txt(String filename) throws IOException {
        // TODO
}
import java.io.*;
import java.util.Scanner;

public class Filter {

    public static void main(String[] args) {
        // Vérification des arguments
        if (args.length < 3) {
            System.err.println("Usage: java Filter <input_file> <output_file> <filter_type>");
            System.err.println("You should provide an input and an output filenames and a filter type. Aborting");
            System.exit(-1);
        }

        String inputFilename = args[0];
        String outputFilename = args[1];
        String filterType = args[2];

        Image source = null;

        // Lecture de l'image
        try {
            source = Image.read(inputFilename);
        } catch (IOException e) {
            System.err.println("Error reading input file: " + e.getMessage());
            System.exit(-1);
        }

        int h = source.getHeight();
        int w = source.getWidth();
        Image destination = new Image(w, h);

        // Application du filtre pixel par pixel
        for (int y = 0; y < h; y++) {
            for (int x = 0; x < w; x++) {
                int rouge = source.getRed(x, y);
                int vert = source.getGreen(x, y);
                int bleu = source.getBlue(x, y);

                switch (filterType.toLowerCase()) {
                    case "copy":
                        // TODO
                        break;
                    case "dark":
                        // TODO
                        break;
                    case "bright":
                        // TODO
                        break;
                    case "grayscale":
                        // TODO
                        break;
                    default:
                        System.err.println("Unknown filter type: " + filterType);
                        System.exit(-1);
                }
            }
        }

        // Sauvegarde de l'image filtrée
        try {
            destination.write(outputFilename);
            System.out.println("Filtered image saved to " + outputFilename);
        } catch (IOException e) {
            System.err.println("Error writing output file: " + e.getMessage());
        }
    }
}

Pourquoi utilise-t-on deux objets Image ? Quelle influence cela a-t-il sur la mémoire ? Que pourrait-on modifier pour optimiser ?

Quelle est l’influence du switch dans la boucle for ? Quelles en sont les conséquences en termes de performance ?

Sauvegarde au format binaire

Vous allez simplement ajouter les fonctions read_bin et write_bin, avec des signatures de fonctions identiques à celles utilisées pour le format texte. De tel sorte que :

Image img = Image.read_bin("input.ppm");  // lecture binaire
img.write_bin("copy.ppm"); // sauvegarde binaire

La différence principale est que les pixels sont stockés directement en binaire, avec une valeur maximale de 255 (unsigned char en C et byte en Java), sans espaces ni retours à la ligne. Le header fonctionne de manière identique :

P6
largeur hauteur
255
# Les données des pixels suivent immédiatement en binaire
# Chaque pixel est codé sur 3 octets : rouge, vert, bleu.

Le format binaire (P6) présente plusieurs avantages. Tout d’abord, il permet de réduire la taille des fichiers, car les pixels sont stockés directement en binaire. Ensuite, la lecture et l’écriture des images sont plus rapides, puisqu’il n’est pas nécessaire de parser ou de convertir des nombres en texte. Enfin, ce format est moins sensible aux erreurs de formatage, car il ne dépend pas des espaces ou des retours à la ligne pour séparer les valeurs des pixels.

Conclusion

Vous noterez que la simplicité apparente de Java ne doit pas masquer les coûts sous-jacents liés à la gestion des fichiers. Même si la JVM gère automatiquement l’ouverture, la lecture et la fermeture des fichiers via des flux et des tampons, chaque opération d’E/S entraîne des appels système pouvant être coûteux en temps d’exécution, notamment pour de grands fichiers ou des traitements intensifs. Par exemple :

sequenceDiagram participant J as Programme Java participant JVM as Machine Virtuelle Java participant N as Noyau Linux Note over J,N: Ouverture du fichier J->>+JVM: new FileInputStream("fichier.txt") JVM->>+N: Appel système open() activate N Note over N: Context switch entrant N->>N: Vérification des permissions N->>N: Allocation du descripteur N-->>-JVM: Retour du descripteur deactivate N JVM-->>-J: Retour du FileInputStream Note over J,N: Lecture du fichier J->>+JVM: read() JVM->>+N: Appel système read() activate N Note over N: Context switch entrant N->>N: Vérification du descripteur N->>N: Lecture depuis le disque N-->>-JVM: Retour des données deactivate N JVM-->>-J: Retour des données lues Note over J,N: Fermeture du fichier J->>+JVM: close() JVM->>+N: Appel système close() activate N Note over N: Context switch entrant N->>N: Libération du descripteur N-->>-JVM: Confirmation deactivate N JVM-->>-J: Confirmation

De plus, l’utilisation des exceptions, bien qu’utile pour structurer le code, n’apporte aucun avantage réel par rapport à la gestion classique des erreurs en C dans ce contexte précis, et peut ajouter une surcharge importante de traitement. Ainsi, la performance d’un programme ne dépend pas uniquement de sa complexité algorithmique, mais également d’un certain nombre de facteurs tels que la gestion des entrées/sorties, l’utilisation de la mémoire, la taille des tampons, les appels système et la manière dont le langage ou la bibliothèque gère ces opérations.

Ainsi, comprendre ces différences permet non seulement de choisir le bon outil pour chaque tâche, mais également d’anticiper les comportements liés aux performances, à la mémoire et à la gestion des ressources, en particulier pour des applications manipulant de grandes quantités de données ou nécessitant des traitements en temps réel.