# Algo Prog 2021-2022: TP7 - Pixel art designer
###### tags: `algo` `bourg` `c++` `iut` `tp` `sdl`
Nous proposons dans ce TP de découvrir la bibliothèque graphique SDL2, fréquemment utilisée pour la programmation de jeux vidéo/applis multimédia en langage C et C++. Nous allons implémenter ici un éditeur d'image fortement pixellisée (type mosaïque), comme celle dessinée ci-dessous. L'objectif sera de charger une telle image depuis un fichier, de changer la couleur des carrés, ou encore d'afficher l'image en négatif.

Gardez le même projet que pour le tutoriel, faites un nouveau fichier .cpp et excluez celui du tutoriel de la génération.
### Constantes et variables du programme
Votre fenêtre graphique sera découpée en deux zones: la partie gauche contiendra l'image à proprement parler, la partie droite sera une palette utile pour changer la couleur des carrés de l'image. L'exemple ci-dessous sert d'illustration (l'image ne correspond à rien de précis sur cet exemple).

Commencez par définir les constantes dont vous aurez besoin tout au long du programme:
- CARRE, qui correspondra à la taille en pixels d'un petit carré de la mosaïque. Par défaut, fixez-la à 30.
- LARGEUR et HAUTEUR, qui sont respectivement le nombre de petits carrés de l'image à l'horizontal et à la verticale. Dans notre cas ici, on travaillera avec une image de 12 carrés de large par 16 carrés de haut.
- TAILLEX et TAILLEY qui seront respectivement les largeur et hauteur de la fenêtre graphique en pixels. Dans notre exemple, on prendra TAILLEY=CARRE$\times$ HAUTEUR, et TAILLEX=CARRE$\times$ LARGEUR+120 (pour accueillir la palette graphique).
Dans votre main, vous déclarerez les variables suivantes:
- une fenêtre SDL_Window* et un rendu SDL_Renderer* comme dans le tutoriel.
- un tableau 2D tabcouleur de taille HAUTEUR*LARGEUR et contenant des SDL_Color. Ce tableau de couleurs correspondra au tableau des couleurs des carrés. Nous l'initialiserons plus tard.
Pour information, SDL_Color est un type structure, avec les champs r, g et b correspondant respectivement aux quantités de rouge, vert et bleu qui compose une couleur.
**Correction du TP:**
:::spoiler
````C++
#include <SDL_ttf.h>
#include <SDL.h>
#include "config_sdl.h"
#include <fstream>
#include <iostream>
#include <iomanip>
using namespace std;
const int LARGEUR = 12;
const int HAUTEUR = 16;
const int CARRE = 30;
const int TX = CARRE*LARGEUR + 120;
const int TY = CARRE*HAUTEUR;
const SDL_Color noir = { 0,0,0 };
const SDL_Color blanc = { 255,255,255 };
const SDL_Color rouge = { 255,0,0 };
const SDL_Color bleu = { 0,0,255 };
const SDL_Color jaune = { 255,255,0 };
const SDL_Color vert = { 106,164,30 };
void charger(char* fichier, SDL_Color tabcouleur[HAUTEUR][LARGEUR], SDL_Renderer* rendu);
void afficher_palette(SDL_Renderer* rendu);
void negatif(SDL_Color tabcouleur[HAUTEUR][LARGEUR], SDL_Renderer* rendu);
void sauveBMP(char* nom_fichier, SDL_Renderer* rendu);
int main(int argc, char *argv[]) {
SDL_Color tabcouleur[HAUTEUR][LARGEUR];
SDL_Color couleur;
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
cout << "Echec à l’ouverture";
return 1;
}
if (TTF_Init() == -1) {
return false;
}
//on crée la fenêtre
SDL_Window*win = SDL_CreateWindow("Mon app", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, TX, TY, SDL_WINDOW_SHOWN);
if (win == NULL)
cout << "erreur ouverture fenetre";
//on récupère le rendu de la fenêtre (élément dans lequel on tracera)
SDL_Renderer*rendu = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
charger("Mystere.txt", tabcouleur, rendu);
afficher_palette(rendu);
bool continuer = true;
SDL_Event event;
while (continuer) {
SDL_WaitEvent(&event);
switch (event.type) {
case SDL_QUIT:
continuer = 0;
break;
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_n) { //touche n
negatif(tabcouleur, rendu);
}
break;
case SDL_MOUSEBUTTONUP://appui souris
if (event.button.button == SDL_BUTTON_LEFT) {//si on clique bouton gauche
int x = event.button.x;
int y = event.button.y;
SDL_Rect position;
position.h = position.w = CARRE;
if (x<CARRE*LARGEUR && y<CARRE*HAUTEUR) {
x /= CARRE;
y /= CARRE;
position.x = x*CARRE;
position.y = y*CARRE;
tabcouleur[y][x].b = couleur.b;
tabcouleur[y][x].r = couleur.r;
tabcouleur[y][x].g = couleur.g;
SDL_SetRenderDrawColor(rendu, tabcouleur[y][x].r, tabcouleur[y][x].g, tabcouleur[y][x].b, 255);
SDL_RenderFillRect(rendu, &position);
SDL_RenderPresent(rendu);
}
else if (x<CARRE*LARGEUR + 20 + 2 * (CARRE + 10) && x>CARRE*LARGEUR + 20 && y>20 && y<20 + 3 * (CARRE + 10)) {
y = (y - 20) / (CARRE + 10);
x = (x - CARRE*LARGEUR - 20) / (CARRE + 10);
if (x == 0 && y == 0)
couleur = noir;
else if (x == 0 && y == 1)
couleur = rouge;
else if (x == 0 && y == 2)
couleur = jaune;
else if (x == 1 && y == 0)
couleur = blanc;
else if (x == 1 && y == 1)
couleur = vert;
else if (x == 1 && y == 2)
couleur = bleu;
}
else if (x<CARRE*LARGEUR + 20 + 2 * CARRE + 10 && x>CARRE*LARGEUR + 20 - 10 && y>100 + 3 * (CARRE + 10) - 10 && y<100 + 3 * (CARRE + 10) + 30) {
sauveBMP("mm.bmp", rendu);
}
}
break;
}
}
//on détruit le rendu
SDL_DestroyRenderer(rendu);
//on détruit la fenêtre
SDL_DestroyWindow(win);
TTF_Quit();
SDL_Quit();
}
void charger(char* fichier, SDL_Color tabcouleur[HAUTEUR][LARGEUR], SDL_Renderer* rendu) {
ifstream f(fichier, ios::in);
int i, j, r, g, b;
char c;
SDL_Rect petit_carre;
petit_carre.h = petit_carre.w = CARRE;
if (!f)
cout << "erreur lecture fichier";
else
for (i = 0; i<HAUTEUR; i++) {
for (j = 0; j<LARGEUR; j++) {
f >> c;
switch (c) {
case 'B':r = g = b = 255; break;
case 'N':r = g = b = 0; break;
case 'R':r = 255; g = b = 0; break;
case 'V':r = 106; b = 30; g = 164; break;
case 'J':r = g = 255; b = 0; break;
}
tabcouleur[i][j].r = r;
tabcouleur[i][j].g = g;
tabcouleur[i][j].b = b;
SDL_SetRenderDrawColor(rendu, r, g, b, 255);
petit_carre.x = j*CARRE;
petit_carre.y = i*CARRE;
SDL_RenderFillRect(rendu, &petit_carre);
}
}
SDL_RenderPresent(rendu);
f.close();
}
void negatif(SDL_Color tabcouleur[HAUTEUR][LARGEUR], SDL_Renderer* rendu) {
int i, j;
SDL_Rect petit_carre;
petit_carre.h = petit_carre.w = CARRE;
for (i = 0; i<HAUTEUR; i++) {
for (j = 0; j<LARGEUR; j++) {
tabcouleur[i][j].b = 255 - tabcouleur[i][j].b;
tabcouleur[i][j].g = 255 - tabcouleur[i][j].g;
tabcouleur[i][j].r = 255 - tabcouleur[i][j].r;
SDL_SetRenderDrawColor(rendu, tabcouleur[i][j].r, tabcouleur[i][j].g, tabcouleur[i][j].b, 255);
petit_carre.x = j*CARRE;
petit_carre.y = i*CARRE;
SDL_RenderFillRect(rendu, &petit_carre);
}
}
SDL_RenderPresent(rendu);
}
void afficher_palette(SDL_Renderer* rendu) {
int i, j;
SDL_Rect carre;
SDL_Rect contour;
SDL_Rect positionTexte;
//gestion du contour du carré blanc
contour.h = contour.w = CARRE + 2;
contour.x = CARRE*LARGEUR + 20 - 1;
contour.y = 19;
SDL_SetRenderDrawColor(rendu, 255, 255, 255, 255);
SDL_RenderDrawRect(rendu, &contour);
//bouton export
//on ouvre la police de caractères
TTF_Init();
TTF_Font*font = TTF_OpenFont("C:\\Windows\\Fonts\\calibri.ttf", 12);
//on place le texte au point (100,100)
positionTexte.x = CARRE*LARGEUR + 20;
positionTexte.y = 100 + 3 * (CARRE + 10);
//on crée une texture à partir du texte, de sa couleur, et de la fonte
SDL_Texture*texture = loadText(rendu, "Export BMP", blanc, font);
//on maj le rectangle couvrant cette texture
SDL_QueryTexture(texture, NULL, NULL, &positionTexte.w, &positionTexte.h);
//on copie la texture dans le rendu
SDL_RenderCopy(rendu, texture, NULL, &positionTexte);
positionTexte.w += 20;
positionTexte.h += 20;
positionTexte.x -= 10;
positionTexte.y -= 10;
SDL_RenderDrawRect(rendu, &positionTexte);
SDL_SetRenderDrawColor(rendu, 255, 255, 255, 255);
for (i = 0; i<3; i++)
for (j = 0; j<2; j++) {
carre.h = carre.w = CARRE;
carre.x = CARRE*LARGEUR + 20 + j*(CARRE + 10);
carre.y = 20 + i*(CARRE + 10);
switch (2 * i + j) {
case 0: SDL_SetRenderDrawColor(rendu, 0, 0, 0, 255); break;
case 1: SDL_SetRenderDrawColor(rendu, 255, 255, 255, 255); break;
case 2: SDL_SetRenderDrawColor(rendu, 255, 0, 0, 255); break;
case 3: SDL_SetRenderDrawColor(rendu, 106, 164, 30, 255); break;
case 4: SDL_SetRenderDrawColor(rendu, 255, 255, 0, 255); break;
case 5: SDL_SetRenderDrawColor(rendu, 0, 0, 255, 255); break;
}
SDL_RenderFillRect(rendu, &carre);
}
SDL_RenderPresent(rendu);
}
void sauveBMP(char* nom_fichier, SDL_Renderer* rendu) {
SDL_Surface *sshot = SDL_CreateRGBSurface(0, TX - 120, TY, 32, 0, 0, 0, 0);
SDL_RenderReadPixels(rendu, NULL, SDL_PIXELFORMAT_ARGB8888, sshot->pixels, sshot->pitch);
SDL_SaveBMP(sshot, nom_fichier);
SDL_FreeSurface(sshot);
}
````
:::
### Ouverture d'une fenêtre
Dans votre main, lancez la SDL comme dans le tutoriel, initialisez la fenêtre et le rendu comme vous l'avez fait dans le tutoriel puis ouvrez une fenêtre de taille TAILLEX * TAILLEY. Laissez cette fenêtre visible avec une boucle ``while(continuer)`` et un ``SDL_Event`` comme dans le tutoriel. N'oubliez pas l'instruction ``SDL_Quit()`` avant de quitter le programme, et la destruction de la fenêtre et du rendu.
### Chargement d'une image
On va maintenant chercher à afficher l'image pixellisée qui se trouve dans le fichier [Mystere.txt](https://perso.liris.cnrs.fr/eric.duchene/algo/Mystere.txt). Téléchargez-la et enregistrez-la dans le répertoire de votre projet qui contient le fichier source .cpp.
Ouvrez maintenant ce fichier à l'aide du bloc notes. Vous voyez une présentation de l'image sous forme d'un tableau 2D (de taille HAUTEURxLARGEUR) qui contient les caractères B,N,R,V,J séparés par des espaces, et correspondant aux couleurs de chaque carré. Plus précisément, on a la correspondance suivante:
- B blanc, code RGB 255 255 255
- N noir, code RGB 0 0 0
- R rouge, code RGB 255 0 0
- V vert kaki, code RGB 106 164 30
- J jaune, code RGB 255 255 0
Votre objectif est maintenant de vous débrouiller pour afficher l'image sous-jacente et trouver qui se cache derrière l'image mystère. Pour cela, codez la fonction suivante
``void charger(const char* nom_fichier,
SDL_Color tabcouleur[HAUTEUR][LARGEUR], SDL_Renderer* rendu);``
Cette fonction ouvre le fichier dont le nom est passé en paramètre, et le parcourt en remplissant le tableau tabcouleur passé en paramètre. En parallèle, vous créerez un petit carré de type ``SDL_Rect`` dont les champs ``w(largeur)`` et ``h (hauteur)`` doivent être initialisés à CARRE. Vous le placerez ensuite sur l'écran à la bonne position (champs x et y) avec la fonction ``SDL_RenderFillRect``, en prenant à chaque fois le pinceau de la bonne couleur (càd ``tabcouleur[i][j]``). N'oubliez pas de désallouer la mémoire allouée dynamiquement avant de quitter le programme.
Dans votre main, appelez maintenant votre fonction charger sur le fichier "Mystere.txt".Vous devez voir apparaître l'image mystère codée dans le fichier. De quoi s'agit-t-il ?
### Négatif d'une image
Le négatif d'une image consiste à donner la couleur complémentaire à 255 à chaque carré de notre mosaïque. Plus précisément, si un carré de l'image a pour couleur (R,V,B), sa nouvelle couleur en négatif sera (255-R,255-V,255-B).
Ecrivez la fonction
``void negatif(SDL_Color tabcouleur[HAUTEUR][LARGEUR],SDL_Renderer* rendu)``
qui modifie le tableau tabcouleur passé en paramètre pour fournir le négatif de l'image initiale de la façon suivante:
- Pour le tableau tabcouleur, il suffit de changer chaque ``tabcouleur[i][j]`` en fonction de la définition ci-dessus.
- Votre fonction affiche ensuite l'image en tenant compte des nouvelles couleurs.
- Testez votre fonction sur l'image précédemment chargée.
### Gestion des évènements
Plutôt que d'appeler la fonction ``negatif`` directement dans votre code, on souhaite plutôt que l'utilisateur l'appelle lorsqu'il appuie sur la touche 'n' du clavier.
En utilisant la même boucle de gestion d'évènements que dans le tutoriel, prenez en charge les deux évènements suivants: le clic sur la croix doit quitter le programme, l'appui sur la touche 'n' du clavier doit appeler la fonction ``negatif``.
### Affichage de la palette
La palette graphique permettant de changer la couleur des carrés est visible sur l'exemple screenshot ci-dessus. Elle consiste en la présence de six carrés, correspondant aux 6 couleurs B,N,J,V,R, et bleu.
Codez la fonction
``void charger_palette(SDL_Renderer* rendu) ``
qui va afficher cette palette à l'écran. Testez-la dans votre main.
### Utilisation de la palette
Complétez maintenant votre main pour gérer les événements de type clic souris dans la fenêtre. Lorsque l'utilisateur cliquera sur un des carrés de la palette, cela reviendra à prendre un pinceau de la couleur correspondante. Lorsqu'il cliquera sur un des carrés de la mosaïque, la couleur de ce carré changera pour prendre celle du pinceau.
### Export
Ajoutez maintenant à votre palette un bouton "Export BMP". En cliquant dessus, l'image visible à l'écran doit être exportée vers un fichier bmp. Pour cela, vous utiliserez la fonction ``SDL_SaveBMP`` dont le premier paramètre en de type ``SDL_Surface*`` et correspond au screenshot à exporter. Pour le construire, appelez la fonction ``SDL_CreateRGBSurface`` avec les paramètres à 0 partout sauf les deuxième, troisième (tailles du screenshot) et quatrième (mettre la valeur 32). Pour remplir ce screenshot, on utilisera l'appel suivant:
``SDL_RenderReadPixels(rendu, NULL, SDL_PIXELFORMAT_ARGB8888, sshot->pixels, sshot->pitch)``, où sshot est la variable de type SDL_Surface*. N'oubliez enfin pas de libérer la mémoire de la Surface avec ``SDL_FreeSurface``.