É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

- 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>