Éditeur de photo pour le wiki

C'est un petit éditeur en ligne qui permet d'ajouter des polygones colorés sur les photos afin de délimiter les différentes zones des sites.

Aperçu

Image illustrée

Comment créer une telle photo pour le wiki ?

  • Prenez la photo aérienne du site avec les différentes zones.
  • Ajustez l’éclairage et la colorimétrie pour la rendre aussi belle que possible.
  • Redimensionnez l'image à 976 px de largeur et compressez-la à 50 % en JPEG progressif.
  • Utilisez l'éditeur en ligne pour ajouter les polygones colorés des différentes zones.
  • Envoyez-la sur le wiki.

Compression

Pour éviter que les pages soient trop lourdes à charger, redimensionnez les images à 976 px de largeur et compressez-les à 50 % en JPEG progressif. Idéalement, essayez de ne pas dépasser les 100 Ko.

Si vous êtes sous Linux, cette commande fera le travail :

sudo apt install imagemagick # Pour installer convert la première fois
convert input.jpg -resize 976x -quality 50 -interlace Plane output.jpg

Éditeur en ligne

➡️ Pour dessiner les polygones des différentes zones, ne vous fiez pas aux apparences, utilisez ce modeste petit éditeur en ligne sur ordinateur et enregistrez votre création au format .json.jpg pour l'intégrer au wiki.

Vous pouvez vous entraîner en éditant cette image, par exemple : commes.json.jpg (Cliquez droit, puis "Enregistrer le lien sous..."). Cette image d'exemple contient déjà des polygones, qui ne peuvent être affichés que par l'éditeur et le wiki. Cela vous permettra de voir comment les polygones sont définis, comment en ajouter, en supprimer ou modifier les couleurs.

Vous pouvez également tester avec une image de votre propre choix.

Les polygones de couleurs sont intégrés dans le fichier jpg ?

Oui, c'est toute la subtilité de l'éditeur. Il enregistre la description des polygones en format JSON, et ce JSON est intégré dans le segment EXIF APP1 de votre JPEG, sans altérer la partie qui décrit l'image. Ainsi, vous pouvez modifier votre image autant que vous le souhaitez, et même supprimer certaines zones plus tard, sans avoir à recréer des pixels qui étaient auparavant cachés sous les polygones, et tout tient en un seul fichier.

Les couleurs

Une couleur est un mélange de trois composants : Rouge (R), Vert (G) et Bleu (B). Le format #RRGGBBAA est un code hexadécimal pour représenter ces couleurs et la transparence (alpha) :

RR : La composante rouge (de 00 à FF)
GG : La composante verte (de 00 à FF)
BB : La composante bleue (de 00 à FF)
AA : La transparence (alpha) (de 00 pour totalement transparent à FF pour opaque)

Exemple : #FF000080 = 100% rouge, 0% vert, 0% bleu, 50% transparent. Le jaune est un mélange de rouge et de vert, au sens de la lumière (pas comme en peinture). Ainsi, le jaune opaque s'écrira #FFFF00FF (100% rouge, 100% vert, 0% bleu, 100% opaque).

En hexadécimal, les chiffres vont de 0 à 9, puis de A à F, et on utilisera toujours deux chiffres. Voici comment on compte en hexadécimal : 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 0A, 0B, 0C, 0D, 0E, 0F, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1A, 1B, 1C, 1D, 1E, 1F, 2A, 2B, 2C, 2D ... 78, 79, 7A, 7B, 7C, 7D, 7E, 7F, 80, 81, 82, 83, 84 ... 95, 96, 97, 98, 99, 9A, 9B, 9C, 9D, 9E, 9E, A0, A1, A2, A3, A4, A5, A6 ... F7, F8, F9, FA, FB, FC, FD, FE et FF.

En pratique, pour les couleurs des polygones, on peut se contenter de valeurs 00, 80 et FF qui veulent dire 0 %, 50 % et 100 %.

Sinon, utilisez un sélecteur de couleurs en ligne pour obtenir facilement le code de la couleur souhaitée.

JSON

Le JSON est un format de données structuré et lisible, utilisé ici pour stocker les couleurs et les polygones. En modifiant un polygone dans l'éditeur, vous verrez le JSON se mettre à jour en temps réel. Vous pouvez aussi l'éditer manuellement, mais veillez à respecter sa structure. Si le JSON est malformé, les polygones ne s'afficheront plus. Dans ce cas, utilisez Ctrl + Z pour annuler les changements.

Redimensionner un .json.jpg

Le redimensionnement des images affecte la position des polygones. Cependant, ne vous inquiétez pas, l'éditeur détecte automatiquement les JPEG redimensionnés et ajuste les polygones en fonction des nouvelles proportions de l'image. Pour cela, le segment EXIF APP1 de votre JPEG doit être conservé ; c'est lui qui contient le JSON, qui comporte l'information sur les polygones et la taille de l'image.

Ajouter l'image sur le wiki

Actuellement, c'est moi, David, qui envoie manuellement le fichier sur le wiki. Vous pouvez m'envoyer votre image par mail au format .json.jpg ou l'héberger temporairement sur un serveur externe. Le wiki prend en charge le format .json.jpg et saura afficher les polygones.

Réutilisation des images

Les images disponibles sur wikiparapente.fr sont sous licence CC BY-SA 4.0. Vous pouvez les utiliser dans le respect des conditions de cette licence, qui vous permet de les partager, modifier et redistribuer, à condition de créditer l’auteur, de mentionner la licence et de fournir un lien vers celle-ci.

Pour plus de détails sur les conditions d'utilisation et comment créditer l’œuvre, veuillez consulter notre section Conditions Générales d'Utilisation (CGU).

Sources

Voici les sources de l'éditeur. Vous pouvez les enregistrer dans un fichier editeur.htm. Tout se fait en JS, côté client.

Editeur

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Éditeur de polygones sur JPEG</title>
</head>
<body>
    <input type="file" id="imageLoader" accept="image/*">
    <br />
    <canvas id="canvas"></canvas>
    <br />
    <textarea id="polygonData" rows="15" cols="60"></textarea>
    <br />
    <button id="downloadBtn">Enregistrer l'image en json.jpg</button>
    <br />
    <button id="downloadBtnPolygons">Enregistrer l'image en .jpg</button>

    <br />
    <b>Ouvrir une image :</b> Cliquer sur le champs permettant de choisir un fichier, puis choisir l'image JPEG.
    <br />
    <b>Ajouter un polygone :</b> Avec le clique gauche, dessiner les points du polygone, puis valider avec le clique droit, ou Echap. Le polygone s'ajoute au json.
    <br />
    <b>Supprimer un polygone :</b> Clique droit sur le polygone à supprimer.
    <br />
    <b>Défaire les dernières actions :</b> Ctrl-Z.
    <br />
    <b>Refaire ce qui vient d'être défait par Ctrl-Z :</b> Ctrl-Y. N'importe quelle action faite après un Ctrl-Z ne permet plus de refaire ce qui a été fait précédemment.
    <br />
    <b>Ajouter un texte au polygone :</b> dans le json, modifier la valeur du champs "text" du polygone.
    <br />
    <b>Mettre le text en gras :</b> dans le json, remplacer par true la valeur du champs "bold" du polygone.
    <br />
    <b>Modifier la taille du text :</b> dans le json, modifier la valeur du champs "textSize" du polygone.
    <br />
    <b>Modifier la largeur du contour du polygone :</b> dans le json, modifier la valeur du champs "lineWidth" du polygone.
    <br />
    <b>Modifier la couleur du contour du polygone :</b> dans le json, modifier la valeur du champs "strokeColor" du polygone, au format #RRGGBBAA.
    <br />
    <b>Modifier la couleur du remplissage du polygone :</b> dans le json, modifier la valeur du champs "fillColor" du polygone, au format #RRGGBBAA.
    <br />
    <b>Enregistrer l'image :</b> Cliquer sur l'un des boutons "Enregistrer l'image en...". La version .json.jpg embarque le JSON dans le segment EXIF APP1 et ne dégrade pas la qualité de l'image, ce qui permet de modifier l'image plus tard. Dans la version .jpg, l'image est affectée par les nouveaux polygones, il ne sera donc pas possible de supprimer les polygones ensuite.
    <br />


    <script>
        const imageLoader = document.getElementById('imageLoader');
        const canvas = document.getElementById('canvas');
        const ctx = canvas.getContext('2d');
        const downloadBtn = document.getElementById('downloadBtn');
        const downloadBtnPolygons = document.getElementById('downloadBtnPolygons');
        const polygonDataElement = document.getElementById('polygonData');

        let originalImageFile = null;
        let img = null;
        let polygonsConfig = {
            polygons: []
        };
        let currentPolygon = null; // Polygone en cours de dessin

        imageLoader.addEventListener('change', handleImage);
        downloadBtn.addEventListener('click', downloadImage);
        downloadBtnPolygons.addEventListener('click', downloadImagePolygons);
        polygonDataElement.addEventListener('input', updatePolygonsFromTextarea);
        canvas.addEventListener('click', addPointToPolygon);
        canvas.addEventListener('contextmenu', handleRightClick); // Écoute du clic droit


    /*function setTextarea(newText) {
        if (polygonDataElement.value === newText) return; // Évite les modifications inutiles

        // Simule une vraie modification utilisateur pour ne pas casser Ctrl+Z
        polygonDataElement.selectionStart = 0;
        polygonDataElement.selectionEnd = polygonDataElement.value.length;

        polygonDataElement.focus();
        document.execCommand("insertText", false, newText);
    }*/


    function setTextarea(newText) {
        if (polygonDataElement.value === newText) return; // Évite les modifications inutiles

        // Sauvegarde de la position de défilement de la fenêtre
        const scrollPosition = window.scrollY;

        // Sauvegarde de la position de défilement avant modification
        const scrollPositionTextarea = polygonDataElement.scrollTop;

        // Empêche le défilement de la fenêtre pendant la modification
        document.body.style.overflow = 'hidden';

        // Simule une vraie modification utilisateur pour ne pas casser Ctrl+Z
        polygonDataElement.selectionStart = 0;
        polygonDataElement.selectionEnd = polygonDataElement.value.length;

        polygonDataElement.focus();
        document.execCommand("insertText", false, newText);

        // Restaure le défilement de la fenêtre et la position de défilement
        document.body.style.overflow = '';
        window.scrollTo(0, scrollPosition);

        // Restaure la position de défilement après modification
        polygonDataElement.scrollTop = scrollPositionTextarea;
    }








        /*function handleImage(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = function (e) {
                img = new Image();
                img.onload = function () {
                    canvas.width = img.width;
                    canvas.height = img.height;
                    drawImageAndPolygons();
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        }*/







        function handleImage(event) {
        const file = event.target.files[0];
        if (!file) return;

        originalImageFile = file;

        const reader = new FileReader();
        reader.onload = function (e) {
        const arrayBuffer = e.target.result;
        const jsonData = extractExifJson(new Uint8Array(arrayBuffer));

        // Afficher les métadonnées dans le textarea
        polygonDataElement.value = jsonData || "";
        polygonsConfig = JSON.parse(jsonData) || {"polygons":[]};
        updatePolygonsFromTextarea();

        // Charger l'image sur le canvas
        const blob = new Blob([arrayBuffer], { type: file.type });
        const imgUrl = URL.createObjectURL(blob);
        img = new Image();

        img.onload = function () {
        let prevWidth = polygonsConfig.size?.width || img.width;
        let prevHeight = polygonsConfig.size?.height || img.height;

        let scaleX = img.width / prevWidth;
        let scaleY = img.height / prevHeight;

        // Réajuster les coordonnées si la taille a changé
        if (img.width !== prevWidth || img.height !== prevHeight) {
        polygonsConfig.polygons.forEach(polygon => {
            polygon.points.forEach(point => {
                point.x *= scaleX;
                point.y *= scaleY;
            });

            // Ajuster la taille des textes
            polygon.textSize *= scaleY;
        });

        }

        // Mettre à jour la taille de l'image dans le JSON
        polygonsConfig.size = {
        width: img.width,
        height: img.height
        };

        // Mettre à jour le textarea
        setTextarea(JSON.stringify(polygonsConfig, null, 2));

        // Mettre à jour le canvas
        canvas.width = img.width;
        canvas.height = img.height;
        drawImageAndPolygons();
        };

        img.src = imgUrl;
        };
        reader.readAsArrayBuffer(file);
    }

    // Fonction pour extraire les métadonnées JSON du segment EXIF
    function extractExifJson(jpegData) {
        let offset = 2; // Commence après l'en-tête JPEG (FFD8)
        while (offset < jpegData.length) {
        if (jpegData[offset] !== 0xFF) break; // Fin des segments

        const marker = jpegData[offset + 1];
        const length = (jpegData[offset + 2] << 8) | jpegData[offset + 3];

        if (marker === 0xE1) { // Segment APP1 (EXIF)
            const exifHeader = "Exif\0\0";
            const headerBytes = new TextDecoder().decode(jpegData.subarray(offset + 4, offset + 10));

            if (headerBytes === exifHeader) {
                const jsonBytes = jpegData.subarray(offset + 10, offset + length + 2);
                return new TextDecoder().decode(jsonBytes);
            }
        }
        offset += 2 + length;
        }
        return null; // Aucune métadonnée trouvée
    }






















        function hexToRgba(hex) {
            if (hex.length === 7) hex += "FF"; // Ajoute l'opacité max si non précisé
            let r = parseInt(hex.substring(1, 3), 16);
            let g = parseInt(hex.substring(3, 5), 16);
            let b = parseInt(hex.substring(5, 7), 16);
            let a = parseInt(hex.substring(7, 9), 16) / 255;
            return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
        }

    function applyPolygonFilters() {
        polygonsConfig.polygons.forEach(polygon => {
        ctx.fillStyle = polygon.fillColor ? hexToRgba(polygon.fillColor) : "rgba(0,255,0,0.4)";
        ctx.strokeStyle = polygon.strokeColor ? hexToRgba(polygon.strokeColor) : "rgba(0,128,0,1)";

        ctx.lineWidth = polygon.lineWidth;

        // Si le polygone n'a qu'un seul point, juste le dessiner
        if (polygon.points.length === 1) {
            ctx.beginPath();
            ctx.rect(polygon.points[0].x - 1, polygon.points[0].y - 1, 2, 2); // Dessiner un petit carré de 2x2
            ctx.fill();
            ctx.stroke();
        } else {
            // Sinon, dessiner le polygone complet
            ctx.beginPath();
            ctx.moveTo(polygon.points[0].x, polygon.points[0].y);
            for (let i = 1; i < polygon.points.length; i++) {
                ctx.lineTo(polygon.points[i].x, polygon.points[i].y);
            }
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
        }

        // Ajouter le texte au centre horizontalement du polygone
        if (polygon.text) {
            ctx.fillStyle = polygon.textColor;
            const fontWeight = polygon.bold ? "bold" : "normal";
            ctx.font = `${fontWeight} ${polygon.textSize}px Arial`;
            const center = getPolygonCenter(polygon.points);
            const textWidth = ctx.measureText(polygon.text).width;
            const textHeight = polygon.textSize;
            const centerX = center.x - textWidth / 2; // Ajuste l'alignement horizontal
            const centerY = center.y + textHeight / 4; // Ajuste l'alignement vertical
            ctx.fillText(polygon.text, centerX, centerY);
        }
        });

        // Dessiner le polygone en cours de création
        if (currentPolygon) {
        ctx.strokeStyle = hexToRgba(currentPolygon.strokeColor);
        ctx.lineWidth = currentPolygon.lineWidth;
        ctx.fillStyle = hexToRgba(currentPolygon.fillColor);

        // Si le polygone en cours n'a qu'un seul point, juste le dessiner
        if (currentPolygon.points.length === 1) {
            ctx.beginPath();
            ctx.rect(currentPolygon.points[0].x - 1, currentPolygon.points[0].y - 1, 2, 2); // Dessiner un petit carré de 2x2

            ctx.fill();
            ctx.stroke();
        } else {
            // Sinon, dessiner le polygone complet
            ctx.beginPath();
            ctx.moveTo(currentPolygon.points[0].x, currentPolygon.points[0].y);
            for (let i = 1; i < currentPolygon.points.length; i++) {
                ctx.lineTo(currentPolygon.points[i].x, currentPolygon.points[i].y);
            }
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
        }
        }
    }


        function getPolygonCenter(points) {
            let x = 0;
            let y = 0;
            points.forEach(point => {
                x += point.x;
                y += point.y;
            });
            return { x: x / points.length, y: y / points.length };
        }

        function updateTextarea() {
            //polygonDataElement.value = JSON.stringify(polygonsConfig, null, 2);
            setTextarea(JSON.stringify(polygonsConfig, null, 2));
        }

        function updatePolygonsFromTextarea() {
        try {
        const newConfig = JSON.parse(polygonDataElement.value);
        if (newConfig.polygons && Array.isArray(newConfig.polygons)) {
            polygonsConfig = newConfig;
            drawImageAndPolygons();
        } else {
            throw new Error("Format invalide");
        }
        } catch (error) {
        console.error("Erreur dans le JSON : " + error.message);
        drawImageAndPolygons(false);
        }
        }

        function addPointToPolygon(event) {
            const x = event.offsetX;
            const y = event.offsetY;

            if (!currentPolygon) {
                // Si aucun polygone n'est en cours, commence un nouveau polygone
                currentPolygon = {
                    points: [{ x, y }],
                    fillColor: "#00FF0066", // Couleur par défaut
                    strokeColor: "#008000FF", // Couleur par défaut
                    lineWidth: 3,
                    text: "", // Texte du champ de texte
                    textColor: "#FFFFFF", // Couleur du texte
                    textSize: 50, // Taille du texte
                    bold: false // Gras ou non
                };
            } else {
                // Ajouter un point au polygone en cours
                currentPolygon.points.push({ x, y });
            }

        // Dessiner le polygone en cours
        drawImageAndPolygons();
        }

    // Fonction pour terminer le polygone en cours
    function finishPolygon() {
        if (currentPolygon && currentPolygon.points.length >= 3) {
        // Ajouter le polygone terminé à la liste des polygones
        polygonsConfig.polygons.push({
            ...currentPolygon,
            points: currentPolygon.points
        });
        }
        currentPolygon = null;  // Réinitialiser le polygone en cours
        updateTextarea();  // Mettre à jour le JSON
        drawImageAndPolygons();  // Redessiner l'image et les polygones
    }


        function handleRightClick(event) {
            event.preventDefault(); // Empêcher le menu contextuel

            if (currentPolygon) {
            finishPolygon();  // Terminer le polygone en cours
            return;
        }

            const x = event.offsetX;
            const y = event.offsetY;

            for (let i = 0; i < polygonsConfig.polygons.length; i++) {
                const polygon = polygonsConfig.polygons[i];
                if (isPointInPolygon(x, y, polygon)) {
                    polygonsConfig.polygons.splice(i, 1); // Supprimer le polygone
                    updateTextarea(); // Mettre à jour le JSON
                    drawImageAndPolygons(); // Redessiner l'image et les polygones
                    return;
                }
            }
        }




        document.addEventListener('keydown', function(event) {
        if (event.key === 'Escape') {
        finishPolygon();  // Terminer le polygone en cours
        }
    });

        function isPointInPolygon(x, y, polygon) {
            let c = false;
            let j = polygon.points.length - 1;
            for (let i = 0; i < polygon.points.length; i++) {
                if ((polygon.points[i].y > y) !== (polygon.points[j].y > y) &&
                    x < (polygon.points[j].x - polygon.points[i].x) * (y - polygon.points[i].y) / (polygon.points[j].y - polygon.points[i].y) + polygon.points[i].x) {
                    c = !c;
                }
                j = i;
            }
            return c;
        }

        function drawImageAndPolygons(drawPolygon=true) {
            if (img) {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
            }
            if (drawPolygon) {
                applyPolygonFilters();
            }
        }




        function downloadImagePolygons() {
            const link = document.createElement('a');
            link.download = 'image_modifiee.jpg';
            link.href = canvas.toDataURL('image/jpeg', 1.00);
            link.click();
        }









    async function downloadImage() {
        if (!originalImageFile) {
        alert("Aucune image chargée !");
        return;
        }

        // Modifier le nom du fichier
        let fileName = originalImageFile.name;
        if (!fileName.includes(".json")) {
        fileName = fileName.replace(/\.jpg$/, ".json.jpg");
        }

        // Lire l'image d'origine en tant qu'ArrayBuffer
        const reader = new FileReader();
        reader.readAsArrayBuffer(originalImageFile);
        reader.onloadend = function () {
        const jpegData = new Uint8Array(reader.result);

        // Supprimer les anciennes métadonnées EXIF
        const jpegWithoutExif = removeExif(jpegData);

        // Générer un nouveau segment EXIF avec le JSON
        const jsonMeta = polygonDataElement.value;
        const exifSegment = createExifSegment(jsonMeta);

        // Ajouter le nouveau segment EXIF après l'en-tête JPEG
        const newJpegData = insertExifSegment(jpegWithoutExif, exifSegment);

        // Créer un Blob et déclencher le téléchargement
        const newBlob = new Blob([newJpegData], { type: "image/jpeg" });
        const link = document.createElement("a");
        link.download = fileName;
        link.href = URL.createObjectURL(newBlob);
        link.click();
        };
    }


    // Fonction pour supprimer uniquement l'EXIF sans casser le JPEG
    function removeExif(jpegData) {
        let offset = 2; // On saute l'entête JPEG (SOI 0xFFD8)

        while (offset < jpegData.length) {
        if (jpegData[offset] !== 0xFF) break; // Fin des segments

        const marker = jpegData[offset + 1];
        const length = (jpegData[offset + 2] << 8) | jpegData[offset + 3];

        if (marker === 0xE1) {
            // Si on trouve le segment EXIF (APP1), on l'enlève
            return new Uint8Array([
                ...jpegData.slice(0, offset), // Garde l'entête
                ...jpegData.slice(offset + 2 + length) // Garde le reste sans EXIF
            ]);
        }

        offset += 2 + length;
        }

        return jpegData; // Si aucun EXIF trouvé, retourne l'original
    }





    // Crée un segment EXIF contenant un JSON
    function createExifSegment(jsonString) {
        const encoder = new TextEncoder();
        const jsonBytes = encoder.encode(jsonString);

        // EXIF Header : "Exif\0\0"
        const exifHeader = [0x45, 0x78, 0x69, 0x66, 0x00, 0x00];

        // Création du segment APP1 (0xFFE1) avec la taille
        const length = exifHeader.length + jsonBytes.length + 2;
        const exifSegment = new Uint8Array(length + 2);
        exifSegment[0] = 0xFF;
        exifSegment[1] = 0xE1; // APP1 segment
        exifSegment[2] = (length >> 8) & 0xFF;
        exifSegment[3] = length & 0xFF;
        exifSegment.set(exifHeader, 4);
        exifSegment.set(jsonBytes, 10);

        return exifSegment;
    }

    // Insère le segment EXIF après l'en-tête JPEG (FFD8)
    function insertExifSegment(jpegData, exifSegment) {
        const newJpeg = new Uint8Array(jpegData.length + exifSegment.length);
        newJpeg.set(jpegData.subarray(0, 2), 0); // Copie FFD8 (Start of Image)
        newJpeg.set(exifSegment, 2); // Ajout du segment EXIF
        newJpeg.set(jpegData.subarray(2), 2 + exifSegment.length); // Le reste de l'image
        return newJpeg;
    }













    </script>
</body>
</html>

Viewer

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Viewer</title>
</head>
<body>
    <img src="vierville.json.jpg" alt="" />
    <img src="commes.json.jpg" alt="" />

    <script>

    // Fonction pour extraire les métadonnées JSON du segment EXIF
    function extractExifJson(jpegData) {
        let offset = 2; // Commence après l'en-tête JPEG (FFD8)
        while (offset < jpegData.length) {
            if (jpegData[offset] !== 0xFF) break; // Fin des segments

            const marker = jpegData[offset + 1];
            const length = (jpegData[offset + 2] << 8) | jpegData[offset + 3];

            if (marker === 0xE1) { // Segment APP1 (EXIF)
                const exifHeader = "Exif\0\0";
                const headerBytes = new TextDecoder().decode(jpegData.subarray(offset + 4, offset + 10));

                if (headerBytes === exifHeader) {
                    const jsonBytes = jpegData.subarray(offset + 10, offset + length + 2);
                    return new TextDecoder().decode(jsonBytes);
                }
            }
            offset += 2 + length;
        }
        return null; // Aucune métadonnée trouvée
    }

    // Fonction pour appliquer les polygones au canvas
    function applyPolygonFilters(polygonsConfig, ctx) {
        polygonsConfig.polygons.forEach(polygon => {
            // Définir les couleurs et le style du polygone
            ctx.fillStyle = polygon.fillColor ? hexToRgba(polygon.fillColor) : "rgba(0,255,0,0.4)";
            ctx.strokeStyle = polygon.strokeColor ? hexToRgba(polygon.strokeColor) : "rgba(0,128,0,1)";
            ctx.lineWidth = polygon.lineWidth;

            // Dessiner le polygone
            ctx.beginPath();
            ctx.moveTo(polygon.points[0].x, polygon.points[0].y);
            for (let i = 1; i < polygon.points.length; i++) {
                ctx.lineTo(polygon.points[i].x, polygon.points[i].y);
            }
            ctx.closePath();
            ctx.fill();
            ctx.stroke();

            // Ajouter le texte au centre du polygone
            if (polygon.text) {
                ctx.fillStyle = polygon.textColor;
                const fontWeight = polygon.bold ? "bold" : "normal";
                ctx.font = `${fontWeight} ${polygon.textSize}px Arial`;
                const center = getPolygonCenter(polygon.points);
                const textWidth = ctx.measureText(polygon.text).width;
                const textHeight = polygon.textSize;
                const centerX = center.x - textWidth / 2;
                const centerY = center.y + textHeight / 4;
                ctx.fillText(polygon.text, centerX, centerY);
            }
        });
    }

    // Fonction pour obtenir le centre d'un polygone
    function getPolygonCenter(points) {
        let sumX = 0, sumY = 0;
        points.forEach(p => {
            sumX += p.x;
            sumY += p.y;
        });
        return { x: sumX / points.length, y: sumY / points.length };
    }

    // Fonction pour convertir les hex en rgba
    function hexToRgba(hex) {
        if (hex.length === 7) hex += "FF"; // Ajoute l'opacité max si non précisé
        let r = parseInt(hex.substring(1, 3), 16);
        let g = parseInt(hex.substring(3, 5), 16);
        let b = parseInt(hex.substring(5, 7), 16);
        let a = parseInt(hex.substring(7, 9), 16) / 255;
        return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
    }

    // Fonction pour gérer le remplacement des images par un canvas
    function replaceImagesWithCanvas() {
        const images = document.querySelectorAll("img");
        images.forEach(img => {
            // Vérifier si l'image se termine par .json.jpg
            if (img.src.endsWith('.json.jpg')) {
                const canvas = document.createElement("canvas");
                const ctx = canvas.getContext("2d");

                img.onload = function() {
                    // Retarder la transformation d'une seconde (1000 ms)
                    setTimeout(() => {
                        // Redimensionner le canvas en fonction de l'image
                        canvas.width = img.width;
                        canvas.height = img.height;
                        ctx.drawImage(img, 0, 0);

                        // Extraire les métadonnées EXIF et les polygones
                        fetch(img.src)
                            .then(response => response.arrayBuffer())
                            .then(buffer => {
                                const jpegData = new Uint8Array(buffer);
                                const jsonData = extractExifJson(jpegData);

                                if (!jsonData) return; // Aucune donnée EXIF, on ne remplace pas

                                let polygonsData;
                                try {
                                    polygonsData = JSON.parse(jsonData);
                                } catch (error) {
                                    console.error("Erreur de parsing JSON EXIF :", error);
                                    return;
                                }

                                // Appliquer les polygones au canvas
                                applyPolygonFilters(polygonsData, ctx);

                                // Remplacer l'image par le canvas
                                img.parentNode.replaceChild(canvas, img);
                            })
                            .catch(error => {
                                console.error("Erreur lors de la récupération de l'image :", error);
                            });
                    }, 0); // Un setTimeout de 0 ms permet au fetch de recharger l'image depuis le cache
                };
            }
        });
    }

    // Appeler la fonction
    replaceImagesWithCanvas();

    </script>
</body>
</html>

Intégration

        <script>
            // Fonction pour extraire les métadonnées JSON du segment EXIF
            function extractExifJson(jpegData) {
                let offset = 2; // Commence après l'en-tête JPEG (FFD8)
                while (offset < jpegData.length) {
                    if (jpegData[offset] !== 0xFF) break; // Fin des segments

                    const marker = jpegData[offset + 1];
                    const length = (jpegData[offset + 2] << 8) | jpegData[offset + 3];

                    if (marker === 0xE1) { // Segment APP1 (EXIF)
                        const exifHeader = "Exif\0\0";
                        const headerBytes = new TextDecoder().decode(jpegData.subarray(offset + 4, offset + 10));

                        if (headerBytes === exifHeader) {
                            const jsonBytes = jpegData.subarray(offset + 10, offset + length + 2);
                            return new TextDecoder().decode(jsonBytes);
                        }
                    }
                    offset += 2 + length;
                }
                return null; // Aucune métadonnée trouvée
            }

            // Fonction pour appliquer les polygones au canvas
            function applyPolygonFilters(polygonsConfig, ctx) {
                polygonsConfig.polygons.forEach(polygon => {
                    // Définir les couleurs et le style du polygone
                    ctx.fillStyle = polygon.fillColor ? hexToRgba(polygon.fillColor) : "rgba(0,255,0,0.4)";
                    ctx.strokeStyle = polygon.strokeColor ? hexToRgba(polygon.strokeColor) : "rgba(0,128,0,1)";
                    ctx.lineWidth = polygon.lineWidth;

                    // Dessiner le polygone
                    ctx.beginPath();
                    ctx.moveTo(polygon.points[0].x, polygon.points[0].y);
                    for (let i = 1; i < polygon.points.length; i++) {
                        ctx.lineTo(polygon.points[i].x, polygon.points[i].y);
                    }
                    ctx.closePath();
                    ctx.fill();
                    ctx.stroke();

                    // Ajouter le texte au centre du polygone
                    if (polygon.text) {
                        ctx.fillStyle = polygon.textColor;
                        const fontWeight = polygon.bold ? "bold" : "normal";
                        ctx.font = `${fontWeight} ${polygon.textSize}px Arial`;
                        const center = getPolygonCenter(polygon.points);
                        const textWidth = ctx.measureText(polygon.text).width;
                        const textHeight = polygon.textSize;
                        const centerX = center.x - textWidth / 2;
                        const centerY = center.y + textHeight / 4;
                        ctx.fillText(polygon.text, centerX, centerY);
                    }
                });
            }

            // Fonction pour obtenir le centre d'un polygone
            function getPolygonCenter(points) {
                let sumX = 0, sumY = 0;
                points.forEach(p => {
                    sumX += p.x;
                    sumY += p.y;
                });
                return { x: sumX / points.length, y: sumY / points.length };
            }

            // Fonction pour convertir les hex en rgba
            function hexToRgba(hex) {
                if (hex.length === 7) hex += "FF"; // Ajoute l'opacité max si non précisé
                let r = parseInt(hex.substring(1, 3), 16);
                let g = parseInt(hex.substring(3, 5), 16);
                let b = parseInt(hex.substring(5, 7), 16);
                let a = parseInt(hex.substring(7, 9), 16) / 255;
                return `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`;
            }

            // Fonction pour gérer le remplacement des images par un canvas
            function replaceImagesWithCanvas() {
                const images = document.querySelectorAll("img");
                images.forEach(img => {
                    // Vérifier si l'image se termine par .json.jpg
                    if (img.src.endsWith('.json.jpg')) {

                        img.decode().then(() => {


                        const canvas = document.createElement("canvas");
                        const ctx = canvas.getContext("2d");
                        //img.onload = function() {
                            // Retarder la transformation d'une seconde (1000 ms)
                            //setTimeout(() => {
                                // Redimensionner le canvas en fonction de l'image
                                canvas.width = img.naturalWidth;
                                canvas.height = img.naturalHeight;
                                ctx.drawImage(img, 0, 0);

                                // Extraire les métadonnées EXIF et les polygones
                                fetch(img.src)
                                    .then(response => response.arrayBuffer())
                                    .then(buffer => {
                                        const jpegData = new Uint8Array(buffer);
                                        const jsonData = extractExifJson(jpegData);

                                        if (!jsonData) return; // Aucune donnée EXIF, on ne remplace pas

                                        let polygonsData;
                                        try {
                                            polygonsData = JSON.parse(jsonData);
                                        } catch (error) {
                                            console.error("Erreur de parsing JSON EXIF :", error);
                                            return;
                                        }

                                        // Appliquer les polygones au canvas
                                        applyPolygonFilters(polygonsData, ctx);

                                        // Remplacer l'image par le canvas
                                        //img.parentNode.replaceChild(canvas, img);

                        // Convertir le canvas en image (data URL)
                                const imgDataUrl = canvas.toDataURL("image/png");

                                // Créer une nouvelle balise <img> et la remplacer
                                const newImg = document.createElement("img");
                                newImg.src = imgDataUrl;

                                // Remplacer l'image par la nouvelle image
                                img.parentNode.replaceChild(newImg, img);


                                    })
                                    .catch(error => {
                                        console.error("Erreur lors de la récupération de l'image :", error);
                                    });
                            //}, 2000); // Un setTimeout de 0 ms permet au fetch de recharger l'image depuis le cache
                        //};



                        });

                    }
                });
            }

            // Affichage des polygones sur les .json.jpg
            replaceImagesWithCanvas();
        </script>