Historique du code conçu pour le service principal de ce site
Merci surtout à Gemini pour son aide.
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suppresseur de Publicités pour Bordereaux</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0; /* Couleur de fond initiale du papier */
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important; /* Blanc pour la zone sélectionnée */
border: 2px solid #3b82f6; /* Bleu pour indiquer la sélection active */
}
/* Style pour le message d'erreur/info */
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none; /* Caché par défaut */
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success {
background-color: #28a745; /* Vert pour succès */
}
#message-box.error {
background-color: #dc3545; /* Rouge pour erreur */
}
#message-box.info {
background-color: #17a2b8; /* Bleu pour info */
}
/* Styles pour les icônes dans les quadrants (optionnel, pour visibilité) */
.quadrant-icon {
font-size: 10px; /* Ajustez la taille selon vos besoins */
color: #888;
}
/* Dimensions A4 Portrait: 210mm x 297mm. Ratio approx 1:1.414 */
/* Dimensions A4 Paysage: 297mm x 210mm. Ratio approx 1.414:1 */
/* Pour l'affichage, utilisons des tailles plus petites en gardant le ratio */
.portrait {
width: 100px; /* Largeur fixe pour l'icône portrait */
height: 141.4px; /* Hauteur calculée pour garder le ratio A4 */
}
.landscape {
width: 141.4px; /* Largeur fixe pour l'icône paysage */
height: 100px; /* Hauteur calculée pour garder le ratio A4 */
}
</style>
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux</h1>
<p class="text-gray-600 mt-2">Sélectionnez la zone de la publicité à masquer sur votre bordereau.</p>
</header>
<main>
<section class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Choisissez l'orientation et la zone à masquer :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez votre bordereau (image) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="imageUpload" accept="image/*" class="block w-full max-w-xs text-sm text-gray-500
file:mr-4 file:py-2 file:px-4
file:rounded-lg file:border-0
file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700
hover:file:bg-blue-100
mb-4 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<button id="processButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled="">
Masquer la Publicité
</button>
</div>
</section>
<section>
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">3. Résultat :</h2>
<div class="flex flex-col items-center">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
<a id="downloadLink" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Télécharger l'image modifiée
</a>
</div>
</section>
</main>
<footer class="mt-8 text-center">
<p class="text-xs text-gray-500">© 2025 Outil de suppression de publicités. HG: Haut Gauche, HD: Haut Droite, BG: Bas Gauche, BD: Bas Droite.</p>
</footer>
</div>
<script>
// Références aux éléments DOM
const portraitPaper = document.getElementById('portrait-paper');
const landscapePaper = document.getElementById('landscape-paper');
const allQuadrants = document.querySelectorAll('.quadrant');
const imageUpload = document.getElementById('imageUpload');
const processButton = document.getElementById('processButton');
const imageCanvas = document.getElementById('imageCanvas');
const downloadLink = document.getElementById('downloadLink');
const messageBox = document.getElementById('message-box');
const ctx = imageCanvas.getContext('2d');
// État de la sélection
let selectedQuadrantInfo = {
orientation: null, // 'portrait' ou 'landscape'
quadrantIndex: null // 0: TL, 1: TR, 2: BL, 3: BR
};
let uploadedImage = null;
// Fonction pour afficher les messages
function showMessage(text, type = 'info', duration = 3000) {
messageBox.textContent = text;
messageBox.className = ''; // Réinitialiser les classes
messageBox.classList.add(type); // Ajouter la classe de type (success, error, info)
messageBox.style.display = 'block';
setTimeout(() => {
messageBox.style.display = 'none';
}, duration);
}
// Gestion de la sélection des quadrants
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
// Désélectionner tous les quadrants
allQuadrants.forEach(q => q.classList.remove('selected'));
// Sélectionner le quadrant cliqué
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
// Activer le bouton de traitement si une image est chargée et un quadrant est sélectionné
checkProcessButtonState();
showMessage(`Zone sélectionnée: ${selectedQuadrantInfo.orientation}, quadrant ${selectedQuadrantInfo.quadrantIndex + 1}`, 'info');
});
});
// Gestion du téléversement d'image
imageUpload.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
uploadedImage = new Image();
uploadedImage.onload = () => {
// Afficher l'image originale sur le canvas (optionnel, ou attendre le traitement)
// imageCanvas.width = uploadedImage.width;
// imageCanvas.height = uploadedImage.height;
// ctx.drawImage(uploadedImage, 0, 0);
// imageCanvas.style.display = 'block';
checkProcessButtonState();
showMessage('Image chargée avec succès.', 'success');
downloadLink.classList.add('hidden'); // Cacher le lien de téléchargement si une nouvelle image est chargée
};
uploadedImage.onerror = () => {
showMessage('Erreur lors du chargement de l\'image.', 'error');
uploadedImage = null;
checkProcessButtonState();
}
uploadedImage.src = e.target.result;
};
reader.readAsDataURL(file);
} else {
showMessage('Veuillez sélectionner un fichier image valide (JPEG, PNG, GIF, etc.).', 'error');
uploadedImage = null;
imageUpload.value = ''; // Réinitialiser le champ de fichier
checkProcessButtonState();
}
});
// Vérifier si le bouton de traitement doit être activé
function checkProcessButtonState() {
if (uploadedImage && selectedQuadrantInfo.quadrantIndex !== null) {
processButton.disabled = false;
} else {
processButton.disabled = true;
}
}
// Gestion du clic sur le bouton "Masquer la Publicité"
processButton.addEventListener('click', () => {
if (!uploadedImage) {
showMessage('Veuillez d\'abord téléverser une image.', 'error');
return;
}
if (selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Veuillez sélectionner une zone à masquer sur les icônes A4.', 'error');
return;
}
// Configurer le canvas avec les dimensions de l'image
imageCanvas.width = uploadedImage.width;
imageCanvas.height = uploadedImage.height;
// Dessiner l'image originale
ctx.drawImage(uploadedImage, 0, 0);
// Calculer les coordonnées du rectangle blanc
const imgWidth = uploadedImage.width;
const imgHeight = uploadedImage.height;
let rectX, rectY, rectWidth, rectHeight;
rectWidth = imgWidth / 2;
rectHeight = imgHeight / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: // Haut Gauche
rectX = 0;
rectY = 0;
break;
case 1: // Haut Droite
rectX = imgWidth / 2;
rectY = 0;
break;
case 2: // Bas Gauche
rectX = 0;
rectY = imgHeight / 2;
break;
case 3: // Bas Droite
rectX = imgWidth / 2;
rectY = imgHeight / 2;
break;
}
// Dessiner le rectangle blanc
ctx.fillStyle = 'white';
ctx.fillRect(rectX, rectY, rectWidth, rectHeight);
// Afficher le canvas et le lien de téléchargement
imageCanvas.style.display = 'block';
downloadLink.href = imageCanvas.toDataURL('image/png'); // Proposer en PNG par défaut
downloadLink.download = 'bordereau_modifie.png';
downloadLink.classList.remove('hidden');
showMessage('Publicité masquée ! Vous pouvez télécharger l\'image.', 'success');
});
// Initialiser l'état du bouton
checkProcessButtonState();
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suppresseur de Publicités pour Bordereaux</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
cursor: grab; /* Curseur pour indiquer que la zone est déplaçable */
}
#imageCanvas.dragging {
cursor: grabbing;
}
</style>
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux</h1>
<p class="text-gray-600 mt-2">Masquez les publicités sur vos bordereaux Vinted et autres.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Choisissez la zone initiale de la publicité :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez votre bordereau (Image ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="imageUpload" accept="image/*,application/pdf" class="block w-full max-w-xs text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-4 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled="">
Afficher l'image et ajuster la zone
</button>
<div class="flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Masquer la Pub et Télécharger
</button>
</div>
</section>
</main>
</div>
<script>
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const imageUpload = document.getElementById('imageUpload');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const ctx = imageCanvas.getContext('2d');
// Sections UI
const step1Selection = document.getElementById('step1Selection');
const step2Upload = document.getElementById('step2Upload');
const step3Adjust = document.getElementById('step3Adjust');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let uploadedImage = null; // Contiendra l'objet Image (après chargement direct ou conversion PDF)
let pdfDoc = null; // Pour stocker le document PDF chargé
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let isDraggingSelection = false;
let dragStartCoords = { x: 0, y: 0 }; // Coordonnées souris au début du drag
let rectStartCoords = { x: 0, y: 0 }; // Coordonnées du rectangle au début du drag
// --- Fonctions Utilitaires ---
function showMessage(text, type = 'info', duration = 3500) {
messageBox.textContent = text;
messageBox.className = ''; // Reset classes
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(uploadedImage && selectedQuadrantInfo.quadrantIndex !== null);
}
// --- Gestion des Événements ---
// 1. Sélection du Quadrant Initial
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, quadrant ${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez une image/PDF.`, 'info');
});
});
// 2. Téléversement Image/PDF
imageUpload.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
resetCanvasAndState(); // Réinitialiser si un nouveau fichier est chargé
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
uploadedImage = new Image();
uploadedImage.onload = () => {
showMessage('Image chargée. Cliquez sur "Afficher et ajuster".', 'success');
checkInitiateButtonState();
};
uploadedImage.onerror = () => {
showMessage('Erreur de chargement de l\'image.', 'error'); uploadedImage = null;
}
uploadedImage.src = e.target.result;
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc = pdfDoc_;
renderPdfPage(1); // Afficher la première page
}).catch(err => {
showMessage('Erreur chargement PDF: ' + (err.message || err), 'error'); uploadedImage = null;
checkInitiateButtonState();
});
};
fileReader.readAsArrayBuffer(file);
} else {
showMessage('Type de fichier non supporté. Utilisez JPEG, PNG, GIF ou PDF.', 'error');
uploadedImage = null;
imageUpload.value = '';
}
checkInitiateButtonState();
});
function renderPdfPage(pageNum) {
if (!pdfDoc) return;
pdfDoc.getPage(pageNum).then(page => {
const viewport = page.getViewport({ scale: 2.0 }); // Augmenter l'échelle pour une meilleure qualité
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
uploadedImage = new Image();
uploadedImage.onload = () => {
showMessage('Page PDF chargée. Cliquez sur "Afficher et ajuster".', 'success');
checkInitiateButtonState();
};
uploadedImage.onerror = () => {
showMessage('Erreur conversion PDF en image.', 'error'); uploadedImage = null;
}
uploadedImage.src = tempCanvas.toDataURL('image/png'); // Convertir en PNG pour qualité
});
}).catch(err => {
showMessage('Erreur rendu page PDF: ' + (err.message || err), 'error'); uploadedImage = null;
checkInitiateButtonState();
});
}
// 3. Bouton "Afficher l'image et ajuster la zone"
initiateProcessingButton.addEventListener('click', () => {
if (!uploadedImage || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Veuillez sélectionner une zone initiale ET téléverser un fichier.', 'error');
return;
}
setupInteractiveStage();
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle rouge, puis validez.', 'info');
});
function setupInteractiveStage() {
imageCanvas.width = uploadedImage.width;
imageCanvas.height = uploadedImage.height;
// Calcul du rectangle initial basé sur le quadrant
const imgW = uploadedImage.width;
const imgH = uploadedImage.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break; // HG
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break; // HD
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break; // BG
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break; // BD
}
currentSelectionRect.isDefined = true;
drawCanvasWithSelection();
}
function drawCanvasWithSelection(isFinal=false) {
if (!uploadedImage) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(uploadedImage, 0, 0);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
} else {
ctx.strokeStyle = 'red';
ctx.lineWidth = 3;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
// Dessiner une petite poignée (optionnel, pour indiquer la déplaçabilité)
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(currentSelectionRect.x + currentSelectionRect.w/2 - 10, currentSelectionRect.y + currentSelectionRect.h/2 - 10, 20, 20);
}
}
}
// Gestion du glisser-déposer du rectangle de sélection
imageCanvas.addEventListener('mousedown', (e) => {
if (!currentSelectionRect.isDefined || !uploadedImage) return;
const rect = imageCanvas.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) * (imageCanvas.width / rect.width);
const mouseY = (e.clientY - rect.top) * (imageCanvas.height / rect.height);
// Vérifier si le clic est dans le rectangle
if (mouseX >= currentSelectionRect.x && mouseX <= currentSelectionRect.x + currentSelectionRect.w &&
mouseY >= currentSelectionRect.y && mouseY <= currentSelectionRect.y + currentSelectionRect.h) {
isDraggingSelection = true;
imageCanvas.classList.add('dragging');
dragStartCoords.x = mouseX;
dragStartCoords.y = mouseY;
rectStartCoords.x = currentSelectionRect.x;
rectStartCoords.y = currentSelectionRect.y;
}
});
imageCanvas.addEventListener('mousemove', (e) => {
if (!isDraggingSelection || !currentSelectionRect.isDefined || !uploadedImage) return;
const rect = imageCanvas.getBoundingClientRect();
const mouseX = (e.clientX - rect.left) * (imageCanvas.width / rect.width);
const mouseY = (e.clientY - rect.top) * (imageCanvas.height / rect.height);
const deltaX = mouseX - dragStartCoords.x;
const deltaY = mouseY - dragStartCoords.y;
currentSelectionRect.x = rectStartCoords.x + deltaX;
currentSelectionRect.y = rectStartCoords.y + deltaY;
// Contraintes pour que le rectangle ne sorte pas du canvas
currentSelectionRect.x = Math.max(0, Math.min(currentSelectionRect.x, imageCanvas.width - currentSelectionRect.w));
currentSelectionRect.y = Math.max(0, Math.min(currentSelectionRect.y, imageCanvas.height - currentSelectionRect.h));
drawCanvasWithSelection();
});
imageCanvas.addEventListener('mouseup', () => {
if (isDraggingSelection) {
isDraggingSelection = false;
imageCanvas.classList.remove('dragging');
}
});
imageCanvas.addEventListener('mouseleave', () => { // Arrêter le drag si la souris quitte le canvas
if (isDraggingSelection) {
isDraggingSelection = false;
imageCanvas.classList.remove('dragging');
}
});
// 4. Bouton "Masquer la Pub et Télécharger"
applyAndDownloadButton.addEventListener('click', () => {
if (!currentSelectionRect.isDefined || !uploadedImage) {
showMessage('Aucune zone valide définie ou image chargée.', 'error');
return;
}
drawCanvasWithSelection(true); // true pour dessiner le rectangle blanc final
// Logique de téléchargement améliorée pour iOS
try {
const dataURL = imageCanvas.toDataURL('image/png');
const newWindow = window.open();
if (newWindow) {
newWindow.document.write('<img src="' + dataURL + '" alt="Image modifiée" style="max-width:100%;"/> <br><p>Sur mobile, maintenez l\'appui sur l\'image pour l\'enregistrer. Sur ordinateur, clic droit > Enregistrer l\'image sous...</p>');
showMessage('Image prête dans un nouvel onglet.', 'success');
} else {
// Si window.open est bloqué, fournir un lien direct (peut ne pas fonctionner sur iOS pour le téléchargement direct)
const link = document.createElement('a');
link.href = dataURL;
link.download = 'bordereau_modifie.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showMessage('Tentative de téléchargement direct. Si bloqué, vérifiez les pop-ups.', 'info');
}
} catch (err) {
showMessage('Erreur lors de la préparation du téléchargement: ' + (err.message || err), 'error');
}
// Réinitialiser pour une nouvelle opération
// resetCanvasAndState(); // Optionnel: ou laisser l'utilisateur recommencer manuellement
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
// imageCanvas.style.display = 'none'; // Cacher le canvas après téléchargement
});
function resetCanvasAndState() {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
uploadedImage = null;
pdfDoc = null;
currentSelectionRect.isDefined = false;
isDraggingSelection = false;
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
imageUpload.value = ''; // Important pour permettre de re-téléverser le même fichier
// Ne pas réinitialiser selectedQuadrantInfo ici, l'utilisateur peut vouloir l'utiliser à nouveau
}
// Initialisation
checkInitiateButtonState();
</script>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Suppresseur de Publicités pour Bordereaux</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
/* Le curseur sera géré dynamiquement par JavaScript */
}
.handle { /* Style pour les poignées de redimensionnement */
position: absolute; /* Positionné par JS */
width: 10px;
height: 10px;
background-color: rgba(255, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 50%; /* Rond pour une meilleure prise */
z-index: 10; /* Au-dessus du canvas mais sous les messages */
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux</h1>
<p class="text-gray-600 mt-2">Masquez les publicités sur vos bordereaux Vinted et autres.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Choisissez la zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez votre bordereau (Image ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="imageUpload" accept="image/*,application/pdf" class="block w-full max-w-xs text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-4 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher et Ajuster la Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4"> <canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Masquer la Pub et Télécharger
</button>
</section>
</main>
</div>
<script>
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const imageUpload = document.getElementById('imageUpload');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let uploadedImage = null;
let pdfDoc = null;
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null; // 'move', 'resize-tl', 'resize-t', 'resize-tr', etc.
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 10; // Taille des poignées de redimensionnement
let handles = []; // Pour stocker les éléments DOM des poignées
// --- Fonctions Utilitaires ---
function showMessage(text, type = 'info', duration = 3500) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(uploadedImage && selectedQuadrantInfo.quadrantIndex !== null);
}
// --- Gestion des Événements ---
// 1. Sélection du Quadrant Initial
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, quadrant ${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez une image/PDF.`, 'info');
});
});
// 2. Téléversement Image/PDF
imageUpload.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
resetCanvasAndState();
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
uploadedImage = new Image();
uploadedImage.onload = () => {
showMessage('Image chargée. Cliquez sur "Afficher et Ajuster".', 'success');
checkInitiateButtonState();
};
uploadedImage.onerror = () => {
showMessage('Erreur de chargement de l\'image.', 'error'); uploadedImage = null;
}
uploadedImage.src = e.target.result;
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc = pdfDoc_;
renderPdfPage(1);
}).catch(err => {
showMessage('Erreur chargement PDF: ' + (err.message || err), 'error'); uploadedImage = null;
checkInitiateButtonState();
});
};
fileReader.readAsArrayBuffer(file);
} else {
showMessage('Type de fichier non supporté. Utilisez JPEG, PNG, GIF ou PDF.', 'error');
uploadedImage = null; imageUpload.value = '';
}
checkInitiateButtonState();
});
function renderPdfPage(pageNum) {
if (!pdfDoc) return;
pdfDoc.getPage(pageNum).then(page => {
const viewport = page.getViewport({ scale: 2.5 }); // Échelle augmentée pour meilleure qualité
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
uploadedImage = new Image();
uploadedImage.onload = () => {
showMessage('Page PDF chargée. Cliquez sur "Afficher et Ajuster".', 'success');
checkInitiateButtonState();
};
uploadedImage.onerror = () => {
showMessage('Erreur conversion PDF en image.', 'error'); uploadedImage = null;
}
uploadedImage.src = tempCanvas.toDataURL('image/png');
});
}).catch(err => {
showMessage('Erreur rendu page PDF: ' + (err.message || err), 'error'); uploadedImage = null;
checkInitiateButtonState();
});
}
// 3. Bouton "Afficher et Ajuster la Zone"
initiateProcessingButton.addEventListener('click', () => {
if (!uploadedImage || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Veuillez sélectionner une zone initiale ET téléverser un fichier.', 'error');
return;
}
setupInteractiveStage();
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle (déplacez/redimensionnez), puis validez.', 'info');
});
function setupInteractiveStage() {
imageCanvas.width = uploadedImage.width;
imageCanvas.height = uploadedImage.height;
const imgW = uploadedImage.width;
const imgH = uploadedImage.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles();
}
function drawCanvasWithSelectionAndHandles(isFinal = false) {
if (!uploadedImage) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(uploadedImage, 0, 0);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles(); // Cacher les poignées pour l'image finale
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles(); // S'assurer qu'il n'y a pas d'anciennes poignées
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br']; // Top-Left, Top, Top-Right, etc.
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions(); // Positionner correctement
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0) return;
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect(); // Pour positionner les poignées par rapport au canvas affiché
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
handle.style.left = `${positions[type].left}px`;
handle.style.top = `${positions[type].top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) {
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
// Convertir les coordonnées de la souris de l'écran vers le canvas
function getMousePosOnCanvas(event) {
const rect = imageCanvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * (imageCanvas.width / rect.width),
y: (event.clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
// Gestion du Drag & Resize
canvasContainer.addEventListener('mousedown', (e) => {
if (!currentSelectionRect.isDefined || !uploadedImage) return;
const target = e.target;
const mousePos = getMousePosOnCanvas(e);
if (target.classList.contains('handle')) { // Clic sur une poignée
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas && // Clic sur le canvas
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return; // Clic en dehors
}
dragStartCoords = mousePos;
rectStartCoords = { ...currentSelectionRect }; // Copie de l'état initial du rectangle
e.preventDefault(); // Empêcher la sélection de texte, etc.
});
document.addEventListener('mousemove', (e) => { // Écouter sur document pour un drag plus fluide
if (!activeDragAction || !currentSelectionRect.isDefined) {
// Mettre à jour le curseur si on survole le canvas ou les poignées
if (e.target === imageCanvas || e.target.classList.contains('handle')) {
const mousePos = getMousePosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
// Vérifier si on survole une poignée (même si pas en drag)
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
// Convertir les coordonnées de la poignée en coordonnées relatives au canvas
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
// Petite zone de tolérance pour la détection du survol de la poignée
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 2) {
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' && // Si pas sur une poignée, vérifier si sur le rectangle
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
const mousePos = getMousePosOnCanvas(e);
const deltaX = mousePos.x - dragStartCoords.x;
const deltaY = mousePos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords; // Partir de l'état au début du drag
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
// Assurer que la largeur/hauteur ne deviennent pas négatives ou trop petites
const minSize = 20; // Taille minimale pour le rectangle
if (w < minSize) {
if (activeDragAction.includes('l')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
// Contraintes pour que le rectangle ne sorte pas du canvas
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
// S'assurer que w et h ne dépassent pas les limites du canvas après déplacement de x,y
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles();
e.preventDefault();
});
document.addEventListener('mouseup', (e) => {
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default'; // Réinitialiser le curseur
// S'assurer que les poignées sont bien positionnées après le drag
updateHandlesPositions();
}
});
// 4. Bouton "Masquer la Pub et Télécharger"
applyAndDownloadButton.addEventListener('click', () => {
if (!currentSelectionRect.isDefined || !uploadedImage) {
showMessage('Aucune zone valide définie ou image chargée.', 'error');
return;
}
drawCanvasWithSelectionAndHandles(true); // true pour dessiner le rectangle blanc final
try {
const dataURL = imageCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = dataURL;
link.download = 'bordereau_modifie.png';
// Tentative de téléchargement direct
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showMessage('Téléchargement lancé...', 'success');
} catch (err) {
showMessage('Erreur lors de la préparation du téléchargement. Essayez d\'enregistrer l\'image manuellement.', 'error');
// Fallback: ouvrir dans une nouvelle fenêtre si le téléchargement direct échoue ou est bloqué
try {
const dataURL = imageCanvas.toDataURL('image/png');
const newWindow = window.open();
if (newWindow) {
newWindow.document.write('<img src="' + dataURL + '" alt="Image modifiée" style="max-width:100%;"/> <br><p>Sur mobile, maintenez l\'appui sur l\'image pour l\'enregistrer. Sur ordinateur, clic droit > Enregistrer l\'image sous...</p>');
showMessage('Image prête dans un nouvel onglet (téléchargement direct a pu échouer).', 'info');
} else {
showMessage('Impossible d\'ouvrir un nouvel onglet. Veuillez vérifier les bloqueurs de pop-up.', 'error');
}
} catch (fallbackError) {
showMessage('Erreur critique de téléchargement: ' + (fallbackError.message || fallbackError), 'error');
}
}
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
removeHandles(); // Nettoyer les poignées après l'opération
// imageCanvas.style.display = 'none'; // Optionnel: cacher le canvas
});
function resetCanvasAndState() {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
uploadedImage = null;
pdfDoc = null;
currentSelectionRect.isDefined = false;
activeDragAction = null;
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
imageUpload.value = '';
}
// Initialisation
checkInitiateButtonState();
window.addEventListener('resize', () => { // Mettre à jour la position des poignées si la fenêtre est redimensionnée
if (currentSelectionRect.isDefined && handles.length > 0) {
updateHandlesPositions();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Suppresseur de Publicités pour Bordereaux</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior-y: contain; /* Empêche le "pull-to-refresh" sur mobile pendant le drag */
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
touch-action: none; /* Important pour la gestion tactile personnalisée */
}
.handle {
position: absolute;
width: 12px; /* Légèrement plus grand pour le tactile */
height: 12px;
background-color: rgba(255, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 50%;
z-index: 10;
touch-action: none; /* Empêche le défilement lors de l'interaction avec les poignées */
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux</h1>
<p class="text-gray-600 mt-2">Masquez les publicités sur vos bordereaux Vinted et autres.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Choisissez la zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez votre bordereau (Image ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="imageUpload" accept="image/*,application/pdf" class="block w-full max-w-xs text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-4 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher et Ajuster la Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Masquer la Pub et Télécharger
</button>
</section>
</main>
</div>
<script>
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const imageUpload = document.getElementById('imageUpload');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let uploadedImage = null;
let pdfDoc = null;
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null;
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 12; // Augmenté pour le tactile
let handles = [];
// --- Fonctions Utilitaires ---
function showMessage(text, type = 'info', duration = 4000) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(uploadedImage && selectedQuadrantInfo.quadrantIndex !== null);
}
// --- Gestion des Événements ---
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, Q${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez un fichier.`, 'info');
});
});
imageUpload.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
resetCanvasAndState();
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
uploadedImage = new Image();
uploadedImage.onload = () => {
showMessage('Image chargée. Cliquez sur "Afficher et Ajuster".', 'success');
checkInitiateButtonState();
};
uploadedImage.onerror = () => {
showMessage('Erreur de chargement image.', 'error'); uploadedImage = null;
}
uploadedImage.src = e.target.result;
};
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc = pdfDoc_;
renderPdfPage(1);
}).catch(err => {
showMessage('Erreur chargement PDF: ' + (err.message || err), 'error'); uploadedImage = null;
checkInitiateButtonState();
});
};
fileReader.readAsArrayBuffer(file);
} else {
showMessage('Type de fichier non supporté (Image/PDF).', 'error');
uploadedImage = null; imageUpload.value = '';
}
checkInitiateButtonState();
});
function renderPdfPage(pageNum) {
if (!pdfDoc) return;
pdfDoc.getPage(pageNum).then(page => {
const viewport = page.getViewport({ scale: 2.5 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
uploadedImage = new Image();
uploadedImage.onload = () => {
showMessage('Page PDF chargée. Cliquez sur "Afficher et Ajuster".', 'success');
checkInitiateButtonState();
};
uploadedImage.onerror = () => {
showMessage('Erreur conversion PDF.', 'error'); uploadedImage = null;
}
uploadedImage.src = tempCanvas.toDataURL('image/png');
});
}).catch(err => {
showMessage('Erreur rendu PDF: ' + (err.message || err), 'error'); uploadedImage = null;
checkInitiateButtonState();
});
}
initiateProcessingButton.addEventListener('click', () => {
if (!uploadedImage || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Sélectionnez zone ET téléversez un fichier.', 'error');
return;
}
setupInteractiveStage();
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle, puis validez.', 'info');
});
function setupInteractiveStage() {
imageCanvas.width = uploadedImage.width;
imageCanvas.height = uploadedImage.height;
const imgW = uploadedImage.width;
const imgH = uploadedImage.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles();
}
function drawCanvasWithSelectionAndHandles(isFinal = false) {
if (!uploadedImage) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(uploadedImage, 0, 0);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles();
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles();
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br'];
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions();
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0 || !imageCanvas.offsetParent) return; // Vérif que canvas est visible
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect();
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
handle.style.left = `${canvasRect.left + positions[type].left - canvasContainer.getBoundingClientRect().left}px`;
handle.style.top = `${canvasRect.top + positions[type].top - canvasContainer.getBoundingClientRect().top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) {
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
function getEventPosOnCanvas(event) {
const rect = imageCanvas.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX - rect.left) * (imageCanvas.width / rect.width),
y: (clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
function handleInteractionStart(e) {
if (!currentSelectionRect.isDefined || !uploadedImage) return;
const target = e.target;
const eventPos = getEventPosOnCanvas(e);
if (target.classList.contains('handle')) {
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas &&
eventPos.x >= currentSelectionRect.x && eventPos.x <= currentSelectionRect.x + currentSelectionRect.w &&
eventPos.y >= currentSelectionRect.y && eventPos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return;
}
if (activeDragAction && e.cancelable) { // Empêcher le défilement SEULEMENT si une action de drag/resize commence
e.preventDefault();
}
dragStartCoords = eventPos;
rectStartCoords = { ...currentSelectionRect };
}
function handleInteractionMove(e) {
if (!activeDragAction || !currentSelectionRect.isDefined) {
// Gérer le curseur pour la souris même si pas en drag
if (e.type === 'mousemove' && (e.target === imageCanvas || e.target.classList.contains('handle'))) {
const mousePos = getEventPosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 1.5) { // Tolérance augmentée
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' &&
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
if (e.cancelable) {
e.preventDefault(); // Empêcher le défilement pendant le drag/resize actif
}
const eventPos = getEventPosOnCanvas(e);
const deltaX = eventPos.x - dragStartCoords.x;
const deltaY = eventPos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords;
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
const minSize = 20;
if (w < minSize) {
if (activeDragAction.includes('l') || activeDragAction.includes('tl') || activeDragAction.includes('bl')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t') || activeDragAction.includes('tl') || activeDragAction.includes('tr')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles();
}
function handleInteractionEnd(e) {
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default';
updateHandlesPositions();
}
}
// Attacher les écouteurs d'événements pour souris et tactile
canvasContainer.addEventListener('mousedown', handleInteractionStart);
document.addEventListener('mousemove', handleInteractionMove);
document.addEventListener('mouseup', handleInteractionEnd);
canvasContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
document.addEventListener('touchmove', handleInteractionMove, { passive: false });
document.addEventListener('touchend', handleInteractionEnd);
applyAndDownloadButton.addEventListener('click', () => {
if (!currentSelectionRect.isDefined || !uploadedImage) {
showMessage('Zone non définie ou image non chargée.', 'error');
return;
}
drawCanvasWithSelectionAndHandles(true);
try {
const dataURL = imageCanvas.toDataURL('image/png');
const link = document.createElement('a');
link.href = dataURL;
link.download = 'bordereau_modifie.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showMessage('Téléchargement lancé...', 'success');
} catch (err) {
showMessage('Erreur téléchargement. Essayez d\'enregistrer l\'image manuellement.', 'error');
try {
const dataURL = imageCanvas.toDataURL('image/png');
const newWindow = window.open();
if (newWindow) {
newWindow.document.open();
newWindow.document.write(
'<!DOCTYPE html><html lang="fr"><head><meta name="viewport" content="width=device-width, initial-scale=1.0">' +
'<title>Image Modifiée</title><style>body{margin:0; display:flex; flex-direction:column; justify-content:center; align-items:center; min-height:100vh; background-color:#f0f0f0; text-align:center; font-family:sans-serif;} img{max-width:95%; max-height:80vh; object-fit:contain; box-shadow: 0 0 10px rgba(0,0,0,0.2); margin-bottom:15px;} p{color:#333;}</style></head>' +
'<body><img src="' + dataURL + '" alt="Image modifiée"/><p>Sur mobile (iOS/Android): Appuyez longuement sur l\'image et choisissez "Ajouter à Photos" ou "Télécharger l\'image".<br>Sur ordinateur: Clic droit > "Enregistrer l\'image sous...".</p></body></html>'
);
newWindow.document.close();
showMessage('Image prête dans un nouvel onglet. Suivez les instructions pour enregistrer.', 'info');
} else {
showMessage('Impossible d\'ouvrir un nouvel onglet. Vérifiez les bloqueurs de pop-up.', 'error');
}
} catch (fallbackError) {
showMessage('Erreur critique de téléchargement: ' + (fallbackError.message || fallbackError), 'error');
}
}
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
removeHandles();
});
function resetCanvasAndState() {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
uploadedImage = null;
pdfDoc = null;
currentSelectionRect.isDefined = false;
activeDragAction = null;
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
imageUpload.value = '';
}
checkInitiateButtonState();
window.addEventListener('resize', () => {
if (currentSelectionRect.isDefined && handles.length > 0 && imageCanvas.offsetParent) {
updateHandlesPositions();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Suppresseur de Publicités pour Bordereaux (Batch)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior-y: contain;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
touch-action: none;
}
.handle {
position: absolute;
width: 14px; /* Encore un peu plus grand pour le tactile */
height: 14px;
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
z-index: 10;
touch-action: none;
}
/* Style pour le loader */
#loader {
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #3498db; /* Blue */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 10px auto;
display: none; /* Caché par défaut */
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux (Batch)</h1>
<p class="text-gray-600 mt-2">Masquez les pubs sur plusieurs fichiers à la fois.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez vos fichiers (Images ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="fileUpload" accept="image/*,application/pdf" multiple class="block w-full max-w-md text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-2 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<div class="my-2 text-sm text-gray-600">Format de sortie pour images :
<select id="imageOutputFormat" class="ml-2 p-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
<option value="image/png">PNG (Défaut)</option>
<option value="image/jpeg">JPEG</option>
</select>
</div>
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone sur le 1er fichier et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher 1er Fichier et Ajuster Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<div id="loader"></div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Appliquer à Tous et Télécharger
</button>
</section>
</main>
</div>
<script>
const { jsPDF } = window.jspdf; // Accéder à jsPDF depuis l'objet global window.jspdf
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const fileUpload = document.getElementById('fileUpload');
const imageOutputFormatSelect = document.getElementById('imageOutputFormat');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const loader = document.getElementById('loader');
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let allUploadedFiles = []; // Stocke tous les fichiers téléversés
let currentFileForAdjustment = null; // L'objet Image ou PDF du premier fichier pour l'ajustement
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null;
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 14;
let handles = [];
function showMessage(text, type = 'info', duration = 4000) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(allUploadedFiles.length > 0 && selectedQuadrantInfo.quadrantIndex !== null);
}
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, Q${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez des fichiers.`, 'info');
});
});
fileUpload.addEventListener('change', (event) => {
if (event.target.files.length === 0) {
allUploadedFiles = [];
currentFileForAdjustment = null;
resetCanvasAndState(false); // Ne pas réinitialiser la sélection de quadrant
checkInitiateButtonState();
return;
}
allUploadedFiles = Array.from(event.target.files);
currentFileForAdjustment = null; // Sera défini lors du clic sur "Afficher"
resetCanvasAndState(false);
if (allUploadedFiles.length > 0) {
showMessage(`${allUploadedFiles.length} fichier(s) sélectionné(s). Prêt à ajuster le 1er.`, 'success');
}
checkInitiateButtonState();
});
async function loadFileForAdjustment(file) {
return new Promise((resolve, reject) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur chargement image pour ajustement.'));
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Erreur lecture fichier image.'));
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
// Pour l'ajustement, on ne rend que la première page du premier PDF
pdfDoc_.getPage(1).then(page => {
const viewport = page.getViewport({ scale: 2.0 }); // Échelle suffisante pour l'aperçu
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
const img = new Image();
img.onload = () => resolve(img); // On passe une image de la page PDF
img.onerror = () => reject(new Error('Erreur conversion 1ère page PDF en image.'));
img.src = tempCanvas.toDataURL('image/png');
}).catch(reject);
}).catch(reject);
}).catch(reject);
};
fileReader.onerror = () => reject(new Error('Erreur lecture fichier PDF.'));
fileReader.readAsArrayBuffer(file);
} else {
reject(new Error('Type de fichier non supporté pour l\'ajustement.'));
}
});
}
initiateProcessingButton.addEventListener('click', async () => {
if (allUploadedFiles.length === 0 || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Sélectionnez zone ET téléversez au moins un fichier.', 'error');
return;
}
loader.style.display = 'block';
initiateProcessingButton.disabled = true;
try {
currentFileForAdjustment = await loadFileForAdjustment(allUploadedFiles[0]);
setupInteractiveStage(currentFileForAdjustment); // Passe l'image chargée (ou l'image de la 1ère page PDF)
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle sur ce 1er fichier, puis validez.', 'info');
} catch (error) {
showMessage(error.message || 'Erreur chargement du 1er fichier.', 'error');
console.error("Error loading first file for adjustment:", error);
} finally {
loader.style.display = 'none';
initiateProcessingButton.disabled = false; // Réactiver en cas d'erreur
}
});
function setupInteractiveStage(imageToAdjust) { // imageToAdjust est un objet Image
imageCanvas.width = imageToAdjust.width;
imageCanvas.height = imageToAdjust.height;
const imgW = imageToAdjust.width;
const imgH = imageToAdjust.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles(imageToAdjust); // Dessine l'image passée
}
function drawCanvasWithSelectionAndHandles(imageToDraw, isFinal = false) {
if (!imageToDraw) return; // S'assurer qu'il y a une image à dessiner
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(imageToDraw, 0, 0, imageCanvas.width, imageCanvas.height); // S'assurer que l'image est dessinée aux dimensions du canvas
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles();
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles();
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br'];
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions();
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0 || !imageCanvas.offsetParent) return;
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect();
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
// Positionnement relatif au canvasContainer
handle.style.left = `${canvasRect.left + positions[type].left - canvasContainer.getBoundingClientRect().left}px`;
handle.style.top = `${canvasRect.top + positions[type].top - canvasContainer.getBoundingClientRect().top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) { /* ... (inchangé) ... */
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
function getEventPosOnCanvas(event) { /* ... (inchangé) ... */
const rect = imageCanvas.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX - rect.left) * (imageCanvas.width / rect.width),
y: (clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
function handleInteractionStart(e) { /* ... (inchangé, utilise currentFileForAdjustment pour la condition) ... */
if (!currentSelectionRect.isDefined || !currentFileForAdjustment) return; // Modifié ici
const target = e.target;
const eventPos = getEventPosOnCanvas(e);
if (target.classList.contains('handle')) {
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas &&
eventPos.x >= currentSelectionRect.x && eventPos.x <= currentSelectionRect.x + currentSelectionRect.w &&
eventPos.y >= currentSelectionRect.y && eventPos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return;
}
if (activeDragAction && e.cancelable) {
e.preventDefault();
}
dragStartCoords = eventPos;
rectStartCoords = { ...currentSelectionRect };
}
function handleInteractionMove(e) { /* ... (inchangé, utilise currentFileForAdjustment pour dessiner) ... */
if (!activeDragAction || !currentSelectionRect.isDefined) {
if (e.type === 'mousemove' && (e.target === imageCanvas || e.target.classList.contains('handle'))) {
const mousePos = getEventPosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 1.5) {
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' &&
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
if (e.cancelable) {
e.preventDefault();
}
const eventPos = getEventPosOnCanvas(e);
const deltaX = eventPos.x - dragStartCoords.x;
const deltaY = eventPos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords;
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
const minSize = 20;
if (w < minSize) {
if (activeDragAction.includes('l') || activeDragAction.includes('tl') || activeDragAction.includes('bl')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t') || activeDragAction.includes('tl') || activeDragAction.includes('tr')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles(currentFileForAdjustment); // Redessine avec l'image du 1er fichier
}
function handleInteractionEnd(e) { /* ... (inchangé) ... */
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default';
updateHandlesPositions();
}
}
canvasContainer.addEventListener('mousedown', handleInteractionStart);
document.addEventListener('mousemove', handleInteractionMove);
document.addEventListener('mouseup', handleInteractionEnd);
canvasContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
document.addEventListener('touchmove', handleInteractionMove, { passive: false });
document.addEventListener('touchend', handleInteractionEnd);
applyAndDownloadButton.addEventListener('click', async () => {
if (!currentSelectionRect.isDefined || allUploadedFiles.length === 0) {
showMessage('Zone non définie ou aucun fichier chargé.', 'error');
return;
}
loader.style.display = 'block';
applyAndDownloadButton.disabled = true;
let filesProcessedCount = 0;
for (let i = 0; i < allUploadedFiles.length; i++) {
const file = allUploadedFiles[i];
const originalFilename = file.name.substring(0, file.name.lastIndexOf('.')) || file.name;
showMessage(`Traitement de ${file.name} (${i+1}/${allUploadedFiles.length})...`, 'info', 60000); // Longue durée pour le message
try {
if (file.type.startsWith('image/')) {
const img = await loadFileForAdjustment(file); // Réutiliser pour charger l'image
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
tempCtx.drawImage(img, 0, 0);
tempCtx.fillStyle = 'white';
tempCtx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
const outputFormat = imageOutputFormatSelect.value;
const extension = outputFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(outputFormat, outputFormat === 'image/jpeg' ? 0.9 : undefined); // Qualité pour JPEG
triggerDownload(dataURL, `${originalFilename}_modifie.${extension}`);
filesProcessedCount++;
} else if (file.type === 'application/pdf') {
const pdfOutput = new jsPDF();
const fileReader = new FileReader();
// Promesse pour lire le fichier PDF
const arrayBuffer = await new Promise((resolve, reject) => {
fileReader.onload = () => resolve(fileReader.result);
fileReader.onerror = reject;
fileReader.readAsArrayBuffer(file);
});
const pdfDoc = await pdfjsLib.getDocument(new Uint8Array(arrayBuffer)).promise;
const numPages = pdfDoc.numPages;
for (let j = 1; j <= numPages; j++) {
const page = await pdfDoc.getPage(j);
const viewport = page.getViewport({ scale: 2.0 }); // Bonne échelle pour la qualité
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
tempCtx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
const pageDataUrl = tempCanvas.toDataURL('image/png');
if (j > 1) {
pdfOutput.addPage([viewport.width, viewport.height]); // Utiliser les dimensions exactes
}
pdfOutput.addImage(pageDataUrl, 'PNG', 0, 0, viewport.width, viewport.height);
}
pdfOutput.save(`${originalFilename}_modifie.pdf`);
filesProcessedCount++;
}
await new Promise(resolve => setTimeout(resolve, 200)); // Petite pause entre les téléchargements
} catch (err) {
console.error("Erreur traitement fichier:", file.name, err);
showMessage(`Erreur avec ${file.name}: ${err.message || 'Inconnue'}`, 'error');
}
}
loader.style.display = 'none';
applyAndDownloadButton.disabled = false;
if (filesProcessedCount > 0) {
showMessage(`${filesProcessedCount} fichier(s) traité(s) et téléchargé(s).`, 'success');
} else if (allUploadedFiles.length > 0) {
showMessage(`Aucun fichier n'a pu être traité avec succès.`, 'error');
}
// Réinitialisation partielle pour permettre un nouveau lot avec la même sélection
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
// Ne pas appeler resetCanvasAndState() complètement pour garder la sélection
// fileUpload.value = ''; // L'utilisateur peut vouloir re-téléverser
// allUploadedFiles = []; // Sera réinitialisé au prochain téléversement
});
function triggerDownload(dataURL, filename) {
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function resetCanvasAndState(resetQuadrantSelection = true) {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
currentFileForAdjustment = null; // Réinitialiser l'image d'ajustement
// allUploadedFiles = []; // Ne pas réinitialiser ici, se fait au 'change' de fileUpload
currentSelectionRect.isDefined = false;
activeDragAction = null;
initiateProcessingButton.disabled = true; // Sera réactivé par checkInitiateButtonState
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
// fileUpload.value = ''; // Ne pas réinitialiser ici pour permettre à l'utilisateur de voir les fichiers sélectionnés
if (resetQuadrantSelection) {
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
}
}
checkInitiateButtonState();
window.addEventListener('resize', () => {
if (currentSelectionRect.isDefined && handles.length > 0 && imageCanvas.offsetParent) {
updateHandlesPositions();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Suppresseur de Publicités pour Bordereaux (Batch)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior-y: contain;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
touch-action: none;
}
.handle {
position: absolute;
width: 14px;
height: 14px;
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
z-index: 10;
touch-action: none;
}
#loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 10px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux (Batch)</h1>
<p class="text-gray-600 mt-2">Masquez les pubs sur plusieurs fichiers à la fois.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez vos fichiers (Images ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="fileUpload" accept="image/*,application/pdf" multiple class="block w-full max-w-md text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-2 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<div class="my-2 text-sm text-gray-600">Format de sortie pour images :
<select id="imageOutputFormat" class="ml-2 p-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
<option value="image/png">PNG (Défaut)</option>
<option value="image/jpeg">JPEG</option>
<option value="application/pdf">PDF (pour images)</option>
</select>
</div>
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone sur le 1er fichier et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher 1er Fichier et Ajuster Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<div id="loader"></div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Appliquer à Tous et Télécharger
</button>
</section>
</main>
</div>
<script>
const { jsPDF } = window.jspdf;
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const fileUpload = document.getElementById('fileUpload');
const imageOutputFormatSelect = document.getElementById('imageOutputFormat');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const loader = document.getElementById('loader');
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let allUploadedFiles = [];
let currentFileForAdjustment = null;
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null;
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 14;
let handles = [];
function showMessage(text, type = 'info', duration = 4000) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(allUploadedFiles.length > 0 && selectedQuadrantInfo.quadrantIndex !== null);
}
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, Q${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez des fichiers.`, 'info');
});
});
fileUpload.addEventListener('change', (event) => {
if (event.target.files.length === 0) {
allUploadedFiles = [];
currentFileForAdjustment = null;
resetCanvasAndState(false);
checkInitiateButtonState();
return;
}
allUploadedFiles = Array.from(event.target.files);
currentFileForAdjustment = null;
resetCanvasAndState(false);
if (allUploadedFiles.length > 0) {
showMessage(`${allUploadedFiles.length} fichier(s) sélectionné(s). Prêt à ajuster le 1er.`, 'success');
}
checkInitiateButtonState();
});
async function loadFileForAdjustment(file) {
return new Promise((resolve, reject) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur chargement image pour ajustement.'));
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Erreur lecture fichier image.'));
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc_.getPage(1).then(page => {
const viewport = page.getViewport({ scale: 2.0 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur conversion 1ère page PDF en image.'));
img.src = tempCanvas.toDataURL('image/png');
}).catch(reject);
}).catch(reject);
}).catch(reject);
};
fileReader.onerror = () => reject(new Error('Erreur lecture fichier PDF.'));
fileReader.readAsArrayBuffer(file);
} else {
reject(new Error('Type de fichier non supporté pour l\'ajustement.'));
}
});
}
initiateProcessingButton.addEventListener('click', async () => {
if (allUploadedFiles.length === 0 || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Sélectionnez zone ET téléversez au moins un fichier.', 'error');
return;
}
loader.style.display = 'block';
initiateProcessingButton.disabled = true;
try {
currentFileForAdjustment = await loadFileForAdjustment(allUploadedFiles[0]);
setupInteractiveStage(currentFileForAdjustment);
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle sur ce 1er fichier, puis validez.', 'info');
} catch (error) {
showMessage(error.message || 'Erreur chargement du 1er fichier.', 'error');
console.error("Error loading first file for adjustment:", error);
} finally {
loader.style.display = 'none';
initiateProcessingButton.disabled = false;
}
});
function setupInteractiveStage(imageToAdjust) {
imageCanvas.width = imageToAdjust.width;
imageCanvas.height = imageToAdjust.height;
const imgW = imageToAdjust.width;
const imgH = imageToAdjust.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles(imageToAdjust);
}
function drawCanvasWithSelectionAndHandles(imageToDraw, isFinal = false) {
if (!imageToDraw) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(imageToDraw, 0, 0, imageCanvas.width, imageCanvas.height);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles();
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles();
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br'];
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions();
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0 || !imageCanvas.offsetParent) return;
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect();
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
handle.style.left = `${canvasRect.left + positions[type].left - canvasContainer.getBoundingClientRect().left}px`;
handle.style.top = `${canvasRect.top + positions[type].top - canvasContainer.getBoundingClientRect().top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) {
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
function getEventPosOnCanvas(event) {
const rect = imageCanvas.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX - rect.left) * (imageCanvas.width / rect.width),
y: (clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
function handleInteractionStart(e) {
if (!currentSelectionRect.isDefined || !currentFileForAdjustment) return;
const target = e.target;
const eventPos = getEventPosOnCanvas(e);
if (target.classList.contains('handle')) {
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas &&
eventPos.x >= currentSelectionRect.x && eventPos.x <= currentSelectionRect.x + currentSelectionRect.w &&
eventPos.y >= currentSelectionRect.y && eventPos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return;
}
if (activeDragAction && e.cancelable) {
e.preventDefault();
}
dragStartCoords = eventPos;
rectStartCoords = { ...currentSelectionRect };
}
function handleInteractionMove(e) {
if (!activeDragAction || !currentSelectionRect.isDefined) {
if (e.type === 'mousemove' && (e.target === imageCanvas || e.target.classList.contains('handle'))) {
const mousePos = getEventPosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 1.5) {
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' &&
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
if (e.cancelable) {
e.preventDefault();
}
const eventPos = getEventPosOnCanvas(e);
const deltaX = eventPos.x - dragStartCoords.x;
const deltaY = eventPos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords;
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
const minSize = 20;
if (w < minSize) {
if (activeDragAction.includes('l') || activeDragAction.includes('tl') || activeDragAction.includes('bl')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t') || activeDragAction.includes('tl') || activeDragAction.includes('tr')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles(currentFileForAdjustment);
}
function handleInteractionEnd(e) {
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default';
updateHandlesPositions();
}
}
canvasContainer.addEventListener('mousedown', handleInteractionStart);
document.addEventListener('mousemove', handleInteractionMove);
document.addEventListener('mouseup', handleInteractionEnd);
canvasContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
document.addEventListener('touchmove', handleInteractionMove, { passive: false });
document.addEventListener('touchend', handleInteractionEnd);
applyAndDownloadButton.addEventListener('click', async () => {
if (!currentSelectionRect.isDefined || allUploadedFiles.length === 0) {
showMessage('Zone non définie ou aucun fichier chargé.', 'error');
return;
}
loader.style.display = 'block';
applyAndDownloadButton.disabled = true;
let filesProcessedCount = 0;
for (let i = 0; i < allUploadedFiles.length; i++) {
const file = allUploadedFiles[i];
const originalFilename = file.name.substring(0, file.name.lastIndexOf('.')) || file.name;
showMessage(`Traitement de ${file.name} (${i+1}/${allUploadedFiles.length})...`, 'info', 60000);
try {
if (file.type.startsWith('image/')) {
const img = await loadFileForAdjustment(file);
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
tempCtx.drawImage(img, 0, 0);
tempCtx.fillStyle = 'white';
tempCtx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
const outputFormat = imageOutputFormatSelect.value;
if (outputFormat === 'application/pdf') {
const pdfOutput = new jsPDF({
orientation: img.width > img.height ? 'l' : 'p',
unit: 'px', // Using pixels as unit for direct mapping from image dimensions
format: [img.width, img.height]
});
const pageDataUrl = tempCanvas.toDataURL('image/png');
pdfOutput.addImage(pageDataUrl, 'PNG', 0, 0, img.width, img.height);
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else {
const extension = outputFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(outputFormat, outputFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_modifie.${extension}`);
}
filesProcessedCount++;
} else if (file.type === 'application/pdf') {
let pdfOutput; // Declare here, initialize on first page
const fileReader = new FileReader();
const arrayBuffer = await new Promise((resolve, reject) => {
fileReader.onload = () => resolve(fileReader.result);
fileReader.onerror = reject;
fileReader.readAsArrayBuffer(file);
});
const pdfDocInstance = await pdfjsLib.getDocument(new Uint8Array(arrayBuffer)).promise;
const numPages = pdfDocInstance.numPages;
for (let j = 1; j <= numPages; j++) {
const page = await pdfDocInstance.getPage(j);
const viewport = page.getViewport({ scale: 2.0 });
if (j === 1) {
// Initialize jsPDF for *this* PDF file using its first page's dimensions
pdfOutput = new jsPDF({
orientation: viewport.width > viewport.height ? 'l' : 'p',
unit: 'pt', // pdf.js viewport units are points. jsPDF will handle conversion if its internal unit is different.
format: [viewport.width, viewport.height]
});
} else {
// Add subsequent pages with their own dimensions
pdfOutput.addPage([viewport.width, viewport.height], viewport.width > viewport.height ? 'l' : 'p');
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
tempCtx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
const pageDataUrl = tempCanvas.toDataURL('image/png');
pdfOutput.addImage(pageDataUrl, 'PNG', 0, 0, viewport.width, viewport.height);
}
if (pdfOutput) { // Ensure pdfOutput was initialized
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else if (numPages > 0) { // Should not happen if numPages > 0
console.error("pdfOutput was not initialized for PDF:", file.name);
showMessage(`Erreur interne lors de la création du PDF pour ${file.name}`, 'error');
}
filesProcessedCount++;
}
await new Promise(resolve => setTimeout(resolve, 200));
} catch (err) {
console.error("Erreur traitement fichier:", file.name, err);
showMessage(`Erreur avec ${file.name}: ${err.message || 'Inconnue'}`, 'error');
}
}
loader.style.display = 'none';
applyAndDownloadButton.disabled = false;
if (filesProcessedCount > 0) {
showMessage(`${filesProcessedCount} fichier(s) traité(s) et téléchargé(s).`, 'success');
} else if (allUploadedFiles.length > 0) {
showMessage(`Aucun fichier n'a pu être traité avec succès.`, 'error');
}
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
});
function triggerDownload(dataURL, filename) {
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function resetCanvasAndState(resetQuadrantSelection = true) {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
currentFileForAdjustment = null;
currentSelectionRect.isDefined = false;
activeDragAction = null;
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
if (resetQuadrantSelection) {
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
}
}
checkInitiateButtonState();
window.addEventListener('resize', () => {
if (currentSelectionRect.isDefined && handles.length > 0 && imageCanvas.offsetParent) {
updateHandlesPositions();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Suppresseur de Publicités pour Bordereaux (Batch)</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- PDF.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<!-- jsPDF library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior-y: contain;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
touch-action: none;
}
.handle {
position: absolute;
width: 14px;
height: 14px;
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
z-index: 10;
touch-action: none;
}
#loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 10px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800">Suppresseur de Publicités pour Bordereaux (Batch)</h1>
<p class="text-gray-600 mt-2">Masquez les pubs sur plusieurs fichiers à la fois.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez vos fichiers (Images ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="fileUpload" accept="image/*,application/pdf" multiple class="block w-full max-w-md text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-2 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<div class="my-2 text-sm text-gray-600">Format de sortie (si applicable) :
<select id="outputFormatSelect" class="ml-2 p-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
<option value="source">Comme source (PDF reste PDF, Image reste Image)</option>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
<option value="application/pdf">PDF (convertir tout en PDF)</option>
</select>
</div>
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone sur le 1er fichier et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher 1er Fichier et Ajuster Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<div id="loader"></div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Appliquer à Tous et Télécharger
</button>
</section>
</main>
</div>
<script>
const { jsPDF } = window.jspdf;
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const fileUpload = document.getElementById('fileUpload');
const outputFormatSelect = document.getElementById('outputFormatSelect');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const loader = document.getElementById('loader');
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let allUploadedFiles = [];
let currentFileForAdjustment = null;
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null;
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 14;
let handles = [];
// --- Constantes pour PDF A4 (en points, 1pt = 1/72 inch) ---
const A4_WIDTH_PT = 595.28;
const A4_HEIGHT_PT = 841.89;
function showMessage(text, type = 'info', duration = 4000) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(allUploadedFiles.length > 0 && selectedQuadrantInfo.quadrantIndex !== null);
}
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, Q${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez des fichiers.`, 'info');
});
});
fileUpload.addEventListener('change', (event) => {
if (event.target.files.length === 0) {
allUploadedFiles = [];
currentFileForAdjustment = null;
resetCanvasAndState(false);
checkInitiateButtonState();
return;
}
allUploadedFiles = Array.from(event.target.files);
currentFileForAdjustment = null;
resetCanvasAndState(false);
if (allUploadedFiles.length > 0) {
showMessage(`${allUploadedFiles.length} fichier(s) sélectionné(s). Prêt à ajuster le 1er.`, 'success');
}
checkInitiateButtonState();
});
async function loadFileForAdjustment(file) {
return new Promise((resolve, reject) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur chargement image pour ajustement.'));
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Erreur lecture fichier image.'));
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc_.getPage(1).then(page => {
const viewport = page.getViewport({ scale: 2.0 }); // Scale for good quality preview
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur conversion 1ère page PDF en image.'));
img.src = tempCanvas.toDataURL('image/png');
}).catch(reject);
}).catch(reject);
}).catch(reject);
};
fileReader.onerror = () => reject(new Error('Erreur lecture fichier PDF.'));
fileReader.readAsArrayBuffer(file);
} else {
reject(new Error('Type de fichier non supporté pour l\'ajustement.'));
}
});
}
initiateProcessingButton.addEventListener('click', async () => {
if (allUploadedFiles.length === 0 || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Sélectionnez zone ET téléversez au moins un fichier.', 'error');
return;
}
loader.style.display = 'block';
initiateProcessingButton.disabled = true;
try {
currentFileForAdjustment = await loadFileForAdjustment(allUploadedFiles[0]);
setupInteractiveStage(currentFileForAdjustment);
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle sur ce 1er fichier, puis validez.', 'info');
} catch (error) {
showMessage(error.message || 'Erreur chargement du 1er fichier.', 'error');
console.error("Error loading first file for adjustment:", error);
} finally {
loader.style.display = 'none';
initiateProcessingButton.disabled = false;
}
});
function setupInteractiveStage(imageToAdjust) {
imageCanvas.width = imageToAdjust.width;
imageCanvas.height = imageToAdjust.height;
const imgW = imageToAdjust.width;
const imgH = imageToAdjust.height;
// Scale the initial rectangle based on the preview image dimensions
// The currentSelectionRect coordinates will be in the pixel space of this preview image.
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles(imageToAdjust);
}
function drawCanvasWithSelectionAndHandles(imageToDraw, isFinal = false) {
if (!imageToDraw) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(imageToDraw, 0, 0, imageCanvas.width, imageCanvas.height);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles();
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles();
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br'];
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions();
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0 || !imageCanvas.offsetParent) return;
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect();
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
handle.style.left = `${canvasRect.left + positions[type].left - canvasContainer.getBoundingClientRect().left}px`;
handle.style.top = `${canvasRect.top + positions[type].top - canvasContainer.getBoundingClientRect().top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) {
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
function getEventPosOnCanvas(event) {
const rect = imageCanvas.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX - rect.left) * (imageCanvas.width / rect.width),
y: (clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
function handleInteractionStart(e) {
if (!currentSelectionRect.isDefined || !currentFileForAdjustment) return;
const target = e.target;
const eventPos = getEventPosOnCanvas(e);
if (target.classList.contains('handle')) {
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas &&
eventPos.x >= currentSelectionRect.x && eventPos.x <= currentSelectionRect.x + currentSelectionRect.w &&
eventPos.y >= currentSelectionRect.y && eventPos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return;
}
if (activeDragAction && e.cancelable) {
e.preventDefault();
}
dragStartCoords = eventPos;
rectStartCoords = { ...currentSelectionRect };
}
function handleInteractionMove(e) {
if (!activeDragAction || !currentSelectionRect.isDefined) {
if (e.type === 'mousemove' && (e.target === imageCanvas || e.target.classList.contains('handle'))) {
const mousePos = getEventPosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 1.5) {
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' &&
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
if (e.cancelable) {
e.preventDefault();
}
const eventPos = getEventPosOnCanvas(e);
const deltaX = eventPos.x - dragStartCoords.x;
const deltaY = eventPos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords;
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
const minSize = 20;
if (w < minSize) {
if (activeDragAction.includes('l') || activeDragAction.includes('tl') || activeDragAction.includes('bl')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t') || activeDragAction.includes('tl') || activeDragAction.includes('tr')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles(currentFileForAdjustment);
}
function handleInteractionEnd(e) {
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default';
updateHandlesPositions();
}
}
canvasContainer.addEventListener('mousedown', handleInteractionStart);
document.addEventListener('mousemove', handleInteractionMove);
document.addEventListener('mouseup', handleInteractionEnd);
canvasContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
document.addEventListener('touchmove', handleInteractionMove, { passive: false });
document.addEventListener('touchend', handleInteractionEnd);
applyAndDownloadButton.addEventListener('click', async () => {
if (!currentSelectionRect.isDefined || allUploadedFiles.length === 0) {
showMessage('Zone non définie ou aucun fichier chargé.', 'error');
return;
}
loader.style.display = 'block';
applyAndDownloadButton.disabled = true;
let filesProcessedCount = 0;
const chosenOutputFormat = outputFormatSelect.value;
for (let i = 0; i < allUploadedFiles.length; i++) {
const file = allUploadedFiles[i];
const originalFilename = file.name.substring(0, file.name.lastIndexOf('.')) || file.name;
showMessage(`Traitement de ${file.name} (${i+1}/${allUploadedFiles.length})...`, 'info', 60000);
try {
// Determine target format based on original file type and user selection
let targetFormat = chosenOutputFormat;
if (chosenOutputFormat === 'source') {
targetFormat = file.type.startsWith('image/') ? 'image/png' : 'application/pdf'; // Default to PNG for images if "source"
if (file.type.startsWith('image/jpeg')) targetFormat = 'image/jpeg';
}
// --- Process IMAGE type input file ---
if (file.type.startsWith('image/')) {
const img = await loadFileForAdjustment(file);
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
tempCtx.drawImage(img, 0, 0);
tempCtx.fillStyle = 'white';
// Apply selection rectangle. Coordinates are relative to the source image dimensions.
tempCtx.fillRect(currentSelectionRect.x * (img.width / currentFileForAdjustment.width),
currentSelectionRect.y * (img.height / currentFileForAdjustment.height),
currentSelectionRect.w * (img.width / currentFileForAdjustment.width),
currentSelectionRect.h * (img.height / currentFileForAdjustment.height));
if (targetFormat === 'application/pdf') {
const pdfOutput = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
const pageDataUrl = tempCanvas.toDataURL('image/png');
const targetWidthPt = img.width > img.height ? A4_HEIGHT_PT : A4_WIDTH_PT; // Use A4 landscape if image is landscape
const targetHeightPt = img.width > img.height ? A4_WIDTH_PT : A4_HEIGHT_PT;
if(img.width < img.height) { // Portrait image
pdfOutput.internal.pageSize.width = A4_WIDTH_PT;
pdfOutput.internal.pageSize.height = A4_HEIGHT_PT;
} else { // Landscape image
pdfOutput.internal.pageSize.width = A4_HEIGHT_PT;
pdfOutput.internal.pageSize.height = A4_WIDTH_PT;
}
const ratio = Math.min(targetWidthPt / img.width, targetHeightPt / img.height);
const scaledWidth = img.width * ratio;
const scaledHeight = img.height * ratio;
const xOffset = (targetWidthPt - scaledWidth) / 2;
const yOffset = (targetHeightPt - scaledHeight) / 2;
pdfOutput.addImage(pageDataUrl, 'PNG', xOffset, yOffset, scaledWidth, scaledHeight);
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else {
const extension = targetFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(targetFormat, targetFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_modifie.${extension}`);
}
filesProcessedCount++;
// --- Process PDF type input file ---
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
const arrayBuffer = await new Promise((resolve, reject) => {
fileReader.onload = () => resolve(fileReader.result);
fileReader.onerror = reject;
fileReader.readAsArrayBuffer(file);
});
const pdfDocInstance = await pdfjsLib.getDocument(new Uint8Array(arrayBuffer)).promise;
const numPages = pdfDocInstance.numPages;
if (targetFormat === 'image/png' || targetFormat === 'image/jpeg') { // PDF to Single Image (first page)
const page = await pdfDocInstance.getPage(1);
const viewport = page.getViewport({ scale: 2.0 }); // Good scale for image quality
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
// Apply selection rectangle, scaling it from preview image space to current PDF page space
const scaleToPageX = viewport.width / currentFileForAdjustment.width;
const scaleToPageY = viewport.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleToPageX,
currentSelectionRect.y * scaleToPageY,
currentSelectionRect.w * scaleToPageX,
currentSelectionRect.h * scaleToPageY);
const extension = targetFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(targetFormat, targetFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_page1_modifie.${extension}`);
} else { // PDF to PDF (multi-page)
let pdfOutput;
for (let j = 1; j <= numPages; j++) {
const page = await pdfDocInstance.getPage(j);
const viewport = page.getViewport({ scale: 2.0 }); // Render at good resolution
const pageTargetWidthPt = viewport.width > viewport.height ? A4_HEIGHT_PT : A4_WIDTH_PT;
const pageTargetHeightPt = viewport.width > viewport.height ? A4_WIDTH_PT : A4_HEIGHT_PT;
let orientation = viewport.width > viewport.height ? 'l' : 'p';
if (j === 1) {
pdfOutput = new jsPDF({ orientation: orientation, unit: 'pt', format: [pageTargetWidthPt, pageTargetHeightPt] });
} else {
pdfOutput.addPage([pageTargetWidthPt, pageTargetHeightPt], orientation);
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
// Scale selection rectangle from preview image space to current PDF page space
const scaleToPageX = viewport.width / currentFileForAdjustment.width;
const scaleToPageY = viewport.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleToPageX,
currentSelectionRect.y * scaleToPageY,
currentSelectionRect.w * scaleToPageX,
currentSelectionRect.h * scaleToPageY);
const pageDataUrl = tempCanvas.toDataURL('image/png');
// Add image to fit the A4-like page
const ratio = Math.min(pageTargetWidthPt / viewport.width, pageTargetHeightPt / viewport.height);
const scaledWidth = viewport.width * ratio;
const scaledHeight = viewport.height * ratio;
const xOffset = (pageTargetWidthPt - scaledWidth) / 2;
const yOffset = (pageTargetHeightPt - scaledHeight) / 2;
pdfOutput.addImage(pageDataUrl, 'PNG', xOffset, yOffset, scaledWidth, scaledHeight);
}
if (pdfOutput) {
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else if (numPages > 0) {
console.error("pdfOutput was not initialized for PDF:", file.name);
showMessage(`Erreur interne lors de la création du PDF pour ${file.name}`, 'error');
}
}
filesProcessedCount++;
}
await new Promise(resolve => setTimeout(resolve, 200));
} catch (err) {
console.error("Erreur traitement fichier:", file.name, err);
showMessage(`Erreur avec ${file.name}: ${err.message || 'Inconnue'}`, 'error');
}
}
loader.style.display = 'none';
applyAndDownloadButton.disabled = false;
if (filesProcessedCount > 0) {
showMessage(`${filesProcessedCount} fichier(s) traité(s) et téléchargé(s).`, 'success');
} else if (allUploadedFiles.length > 0) {
showMessage(`Aucun fichier n'a pu être traité avec succès.`, 'error');
}
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
});
function triggerDownload(dataURL, filename) {
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function resetCanvasAndState(resetQuadrantSelection = true) {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
currentFileForAdjustment = null;
currentSelectionRect.isDefined = false;
activeDragAction = null;
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
if (resetQuadrantSelection) {
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
}
}
checkInitiateButtonState();
window.addEventListener('resize', () => {
if (currentSelectionRect.isDefined && handles.length > 0 && imageCanvas.offsetParent) {
updateHandlesPositions();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Suppresseur de Publicités pour Bordereaux (Batch)</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- PDF.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<!-- jsPDF library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior-y: contain;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
touch-action: none;
}
.handle {
position: absolute;
width: 14px;
height: 14px;
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
z-index: 10;
touch-action: none;
}
#loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 10px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<div class="flex justify-between items-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800 flex-grow text-center">Suppresseur de Publicités</h1>
<button id="resetAllButton" title="Réinitialiser tous les paramètres" class="p-2 text-gray-500 hover:text-blue-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw">
<path d="M3 2v6h6"/>
<path d="M3.31 15A9 9 0 0 0 20.7 7.5"/>
<path d="M21 22v-6h-6"/>
<path d="M20.7 9A9 9 0 0 0 3.31 16.5"/>
</svg>
</button>
</div>
<p class="text-gray-600 mt-1">Masquez les pubs sur plusieurs fichiers à la fois.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez vos fichiers (Images ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="fileUpload" accept="image/*,application/pdf" multiple class="block w-full max-w-md text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-2 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<div class="my-2 text-sm text-gray-600">Format de sortie (si applicable) :
<select id="outputFormatSelect" class="ml-2 p-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
<option value="source">Comme source (PDF reste PDF, Image reste Image)</option>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
<option value="application/pdf">PDF (convertir tout en PDF)</option>
</select>
</div>
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone sur le 1er fichier et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher 1er Fichier et Ajuster Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<div id="loader"></div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Appliquer à Tous et Télécharger
</button>
</section>
</main>
</div>
<script>
const { jsPDF } = window.jspdf;
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const fileUpload = document.getElementById('fileUpload');
const outputFormatSelect = document.getElementById('outputFormatSelect');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const loader = document.getElementById('loader');
const resetAllButton = document.getElementById('resetAllButton'); // Nouveau bouton
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let allUploadedFiles = [];
let currentFileForAdjustment = null;
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null;
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 14;
let handles = [];
// --- Constantes pour PDF A4 (en points, 1pt = 1/72 inch) ---
const A4_WIDTH_PT = 595.28;
const A4_HEIGHT_PT = 841.89;
function showMessage(text, type = 'info', duration = 4000) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(allUploadedFiles.length > 0 && selectedQuadrantInfo.quadrantIndex !== null);
}
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, Q${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez des fichiers.`, 'info');
});
});
fileUpload.addEventListener('change', (event) => {
if (event.target.files.length === 0) {
allUploadedFiles = [];
currentFileForAdjustment = null;
resetCanvasAndState(false);
checkInitiateButtonState();
return;
}
allUploadedFiles = Array.from(event.target.files);
currentFileForAdjustment = null;
resetCanvasAndState(false);
if (allUploadedFiles.length > 0) {
showMessage(`${allUploadedFiles.length} fichier(s) sélectionné(s). Prêt à ajuster le 1er.`, 'success');
}
checkInitiateButtonState();
});
async function loadFileForAdjustment(file) {
return new Promise((resolve, reject) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur chargement image pour ajustement.'));
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Erreur lecture fichier image.'));
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc_.getPage(1).then(page => {
const viewport = page.getViewport({ scale: 2.0 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur conversion 1ère page PDF en image.'));
img.src = tempCanvas.toDataURL('image/png');
}).catch(reject);
}).catch(reject);
}).catch(reject);
};
fileReader.onerror = () => reject(new Error('Erreur lecture fichier PDF.'));
fileReader.readAsArrayBuffer(file);
} else {
reject(new Error('Type de fichier non supporté pour l\'ajustement.'));
}
});
}
initiateProcessingButton.addEventListener('click', async () => {
if (allUploadedFiles.length === 0 || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Sélectionnez zone ET téléversez au moins un fichier.', 'error');
return;
}
loader.style.display = 'block';
initiateProcessingButton.disabled = true;
try {
currentFileForAdjustment = await loadFileForAdjustment(allUploadedFiles[0]);
setupInteractiveStage(currentFileForAdjustment);
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle sur ce 1er fichier, puis validez.', 'info');
} catch (error) {
showMessage(error.message || 'Erreur chargement du 1er fichier.', 'error');
console.error("Error loading first file for adjustment:", error);
} finally {
loader.style.display = 'none';
initiateProcessingButton.disabled = false;
}
});
function setupInteractiveStage(imageToAdjust) {
imageCanvas.width = imageToAdjust.width;
imageCanvas.height = imageToAdjust.height;
const imgW = imageToAdjust.width;
const imgH = imageToAdjust.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles(imageToAdjust);
}
function drawCanvasWithSelectionAndHandles(imageToDraw, isFinal = false) {
if (!imageToDraw) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(imageToDraw, 0, 0, imageCanvas.width, imageCanvas.height);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles();
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles();
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br'];
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions();
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0 || !imageCanvas.offsetParent) return;
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect();
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
handle.style.left = `${canvasRect.left + positions[type].left - canvasContainer.getBoundingClientRect().left}px`;
handle.style.top = `${canvasRect.top + positions[type].top - canvasContainer.getBoundingClientRect().top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) {
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
function getEventPosOnCanvas(event) {
const rect = imageCanvas.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX - rect.left) * (imageCanvas.width / rect.width),
y: (clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
function handleInteractionStart(e) {
if (!currentSelectionRect.isDefined || !currentFileForAdjustment) return;
const target = e.target;
const eventPos = getEventPosOnCanvas(e);
if (target.classList.contains('handle')) {
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas &&
eventPos.x >= currentSelectionRect.x && eventPos.x <= currentSelectionRect.x + currentSelectionRect.w &&
eventPos.y >= currentSelectionRect.y && eventPos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return;
}
if (activeDragAction && e.cancelable) {
e.preventDefault();
}
dragStartCoords = eventPos;
rectStartCoords = { ...currentSelectionRect };
}
function handleInteractionMove(e) {
if (!activeDragAction || !currentSelectionRect.isDefined) {
if (e.type === 'mousemove' && (e.target === imageCanvas || e.target.classList.contains('handle'))) {
const mousePos = getEventPosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 1.5) {
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' &&
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
if (e.cancelable) {
e.preventDefault();
}
const eventPos = getEventPosOnCanvas(e);
const deltaX = eventPos.x - dragStartCoords.x;
const deltaY = eventPos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords;
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
const minSize = 20;
if (w < minSize) {
if (activeDragAction.includes('l') || activeDragAction.includes('tl') || activeDragAction.includes('bl')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t') || activeDragAction.includes('tl') || activeDragAction.includes('tr')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles(currentFileForAdjustment);
}
function handleInteractionEnd(e) {
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default';
updateHandlesPositions();
}
}
canvasContainer.addEventListener('mousedown', handleInteractionStart);
document.addEventListener('mousemove', handleInteractionMove);
document.addEventListener('mouseup', handleInteractionEnd);
canvasContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
document.addEventListener('touchmove', handleInteractionMove, { passive: false });
document.addEventListener('touchend', handleInteractionEnd);
applyAndDownloadButton.addEventListener('click', async () => {
if (!currentSelectionRect.isDefined || allUploadedFiles.length === 0) {
showMessage('Zone non définie ou aucun fichier chargé.', 'error');
return;
}
loader.style.display = 'block';
applyAndDownloadButton.disabled = true;
let filesProcessedCount = 0;
const chosenOutputFormat = outputFormatSelect.value;
for (let i = 0; i < allUploadedFiles.length; i++) {
const file = allUploadedFiles[i];
const originalFilename = file.name.substring(0, file.name.lastIndexOf('.')) || file.name;
showMessage(`Traitement de ${file.name} (${i+1}/${allUploadedFiles.length})...`, 'info', 60000);
try {
let effectiveOutputFormat = chosenOutputFormat;
if (chosenOutputFormat === 'source') {
effectiveOutputFormat = file.type.startsWith('image/') ? (file.type === 'image/jpeg' ? 'image/jpeg' : 'image/png') : 'application/pdf';
}
if (file.type.startsWith('image/')) {
const img = await loadFileForAdjustment(file);
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
tempCtx.drawImage(img, 0, 0);
tempCtx.fillStyle = 'white';
const scaleX = img.width / currentFileForAdjustment.width;
const scaleY = img.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleX,
currentSelectionRect.y * scaleY,
currentSelectionRect.w * scaleX,
currentSelectionRect.h * scaleY);
if (effectiveOutputFormat === 'application/pdf') {
const pdfOutput = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
const pageDataUrl = tempCanvas.toDataURL('image/png');
let pageOrientation = img.width > img.height ? 'l' : 'p';
let targetWidthPt = pageOrientation === 'l' ? A4_HEIGHT_PT : A4_WIDTH_PT;
let targetHeightPt = pageOrientation === 'l' ? A4_WIDTH_PT : A4_HEIGHT_PT;
pdfOutput.internal.pageSize.width = targetWidthPt;
pdfOutput.internal.pageSize.height = targetHeightPt;
const ratio = Math.min(targetWidthPt / img.width, targetHeightPt / img.height);
const scaledWidth = img.width * ratio;
const scaledHeight = img.height * ratio;
const xOffset = (targetWidthPt - scaledWidth) / 2;
const yOffset = (targetHeightPt - scaledHeight) / 2;
pdfOutput.addImage(pageDataUrl, 'PNG', xOffset, yOffset, scaledWidth, scaledHeight);
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else {
const extension = effectiveOutputFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(effectiveOutputFormat, effectiveOutputFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_modifie.${extension}`);
}
filesProcessedCount++;
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
const arrayBuffer = await new Promise((resolve, reject) => {
fileReader.onload = () => resolve(fileReader.result);
fileReader.onerror = reject;
fileReader.readAsArrayBuffer(file);
});
const pdfDocInstance = await pdfjsLib.getDocument(new Uint8Array(arrayBuffer)).promise;
const numPages = pdfDocInstance.numPages;
if (effectiveOutputFormat === 'image/png' || effectiveOutputFormat === 'image/jpeg') {
const page = await pdfDocInstance.getPage(1); // Only first page for image conversion
const viewport = page.getViewport({ scale: 2.0 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
const scaleToPageX = viewport.width / currentFileForAdjustment.width;
const scaleToPageY = viewport.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleToPageX,
currentSelectionRect.y * scaleToPageY,
currentSelectionRect.w * scaleToPageX,
currentSelectionRect.h * scaleToPageY);
const extension = effectiveOutputFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(effectiveOutputFormat, effectiveOutputFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_page1_modifie.${extension}`);
} else { // PDF to PDF
let pdfOutput;
for (let j = 1; j <= numPages; j++) {
const page = await pdfDocInstance.getPage(j);
const viewport = page.getViewport({ scale: 2.0 });
let pageOrientation = viewport.width > viewport.height ? 'l' : 'p';
let pageTargetWidthPt = pageOrientation === 'l' ? A4_HEIGHT_PT : A4_WIDTH_PT;
let pageTargetHeightPt = pageOrientation === 'l' ? A4_WIDTH_PT : A4_HEIGHT_PT;
if (j === 1) {
pdfOutput = new jsPDF({ orientation: pageOrientation, unit: 'pt', format: [pageTargetWidthPt, pageTargetHeightPt] });
} else {
pdfOutput.addPage([pageTargetWidthPt, pageTargetHeightPt], pageOrientation);
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
const scaleToPageX = viewport.width / currentFileForAdjustment.width;
const scaleToPageY = viewport.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleToPageX,
currentSelectionRect.y * scaleToPageY,
currentSelectionRect.w * scaleToPageX,
currentSelectionRect.h * scaleToPageY);
const pageDataUrl = tempCanvas.toDataURL('image/png');
const ratio = Math.min(pageTargetWidthPt / viewport.width, pageTargetHeightPt / viewport.height);
const scaledWidth = viewport.width * ratio;
const scaledHeight = viewport.height * ratio;
const xOffset = (pageTargetWidthPt - scaledWidth) / 2;
const yOffset = (pageTargetHeightPt - scaledHeight) / 2;
pdfOutput.addImage(pageDataUrl, 'PNG', xOffset, yOffset, scaledWidth, scaledHeight);
}
if (pdfOutput) {
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else if (numPages > 0) {
console.error("pdfOutput was not initialized for PDF:", file.name);
showMessage(`Erreur interne lors de la création du PDF pour ${file.name}`, 'error');
}
}
filesProcessedCount++;
}
await new Promise(resolve => setTimeout(resolve, 200));
} catch (err) {
console.error("Erreur traitement fichier:", file.name, err);
showMessage(`Erreur avec ${file.name}: ${err.message || 'Inconnue'}`, 'error');
}
}
loader.style.display = 'none';
applyAndDownloadButton.disabled = false;
if (filesProcessedCount > 0) {
showMessage(`${filesProcessedCount} fichier(s) traité(s) et téléchargé(s).`, 'success');
} else if (allUploadedFiles.length > 0) {
showMessage(`Aucun fichier n'a pu être traité avec succès.`, 'error');
}
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
});
function triggerDownload(dataURL, filename) {
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// --- Fonction de Réinitialisation Globale ---
resetAllButton.addEventListener('click', () => {
resetApplicationState();
showMessage('Tous les paramètres ont été réinitialisés.', 'info');
});
function resetApplicationState() {
// Réinitialiser la sélection de quadrant
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
// Réinitialiser les fichiers téléversés
allUploadedFiles = [];
fileUpload.value = ''; // Effacer la sélection de l'input file
// Réinitialiser le canvas et l'état associé
resetCanvasAndState(true); // true pour s'assurer que tout est nettoyé
// Réinitialiser le sélecteur de format de sortie à sa valeur par défaut
outputFormatSelect.value = 'source';
// S'assurer que les boutons sont dans leur état initial
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
loader.style.display = 'none';
// Cacher les messages
messageBox.style.display = 'none';
}
function resetCanvasAndState(resetQuadrantSelectionInternal = true) { // Renommé pour éviter conflit
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
currentFileForAdjustment = null;
currentSelectionRect.isDefined = false;
activeDragAction = null;
// La désactivation/masquage des boutons est gérée par resetApplicationState ou le flux normal
if (resetQuadrantSelectionInternal) { // Changé pour utiliser le paramètre interne
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
}
// S'assurer que le bouton d'initiation est correctement (dés)activé
checkInitiateButtonState();
}
checkInitiateButtonState(); // Appel initial
window.addEventListener('resize', () => {
if (currentSelectionRect.isDefined && handles.length > 0 && imageCanvas.offsetParent) {
updateHandlesPositions();
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Suppresseur de Publicités pour Bordereaux (Batch)</title>
<script src="https://cdn.tailwindcss.com"></script>
<!-- PDF.js library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<!-- jsPDF library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<style>
body {
font-family: 'Inter', sans-serif;
overscroll-behavior-y: contain;
}
.a4-paper {
border: 2px solid #000;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
cursor: pointer;
background-color: #f0f0f0;
}
.quadrant {
border: 1px dashed #666;
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.2s ease-in-out;
}
.quadrant:hover {
background-color: #e0e0e0;
}
.quadrant.selected {
background-color: #ffffff !important;
border: 2px solid #3b82f6;
}
#message-box {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 8px;
color: white;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#message-box.success { background-color: #28a745; }
#message-box.error { background-color: #dc3545; }
#message-box.info { background-color: #17a2b8; }
.quadrant-icon { font-size: 10px; color: #888; }
.portrait { width: 100px; height: 141.4px; }
.landscape { width: 141.4px; height: 100px; }
#imageCanvas {
touch-action: none;
}
.handle {
position: absolute;
width: 14px;
height: 14px;
background-color: rgba(255, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
z-index: 10;
touch-action: none;
}
#loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 10px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div id="message-box"></div>
<div class="bg-white p-6 md:p-8 rounded-xl shadow-2xl w-full max-w-3xl">
<header class="mb-6 text-center">
<div class="flex justify-between items-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-800 flex-grow text-center">Suppresseur de Publicités</h1>
<button id="resetAllButtonTop" title="Réinitialiser tous les paramètres" class="p-2 text-gray-500 hover:text-blue-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw">
<path d="M3 2v6h6"/>
<path d="M3.31 15A9 9 0 0 0 20.7 7.5"/>
<path d="M21 22v-6h-6"/>
<path d="M20.7 9A9 9 0 0 0 3.31 16.5"/>
</svg>
</button>
</div>
<p class="text-gray-600 mt-1">Masquez les pubs sur plusieurs fichiers à la fois.</p>
</header>
<main>
<section id="step1Selection" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">1. Zone initiale approximative :</h2>
<div class="flex flex-col sm:flex-row justify-center items-center gap-6 sm:gap-10">
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Portrait</p>
<div id="portrait-paper" class="a4-paper portrait rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="portrait" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="portrait" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
<div>
<p class="text-center text-sm font-medium text-gray-600 mb-1">Paysage</p>
<div id="landscape-paper" class="a4-paper landscape rounded-md overflow-hidden mx-auto">
<div class="quadrant" data-orientation="landscape" data-quadrant="0"><span class="quadrant-icon">HG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="1"><span class="quadrant-icon">HD</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="2"><span class="quadrant-icon">BG</span></div>
<div class="quadrant" data-orientation="landscape" data-quadrant="3"><span class="quadrant-icon">BD</span></div>
</div>
</div>
</div>
</section>
<section id="step2Upload" class="mb-6">
<h2 class="text-xl font-semibold text-gray-700 mb-3 text-center">2. Téléversez vos fichiers (Images ou PDF) :</h2>
<div class="flex flex-col items-center">
<input type="file" id="fileUpload" accept="image/*,application/pdf" multiple class="block w-full max-w-md text-sm text-gray-500
file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold
file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100
mb-2 p-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<div class="my-2 text-sm text-gray-600">Format de sortie (si applicable) :
<select id="outputFormatSelect" class="ml-2 p-1 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-blue-500">
<option value="source">Comme source (PDF reste PDF, Image reste Image)</option>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
<option value="application/pdf">PDF (convertir tout en PDF)</option>
</select>
</div>
</div>
</section>
<section id="step3Adjust" class="mb-6 text-center">
<h2 class="text-xl font-semibold text-gray-700 mb-3">3. Ajustez la zone sur le 1er fichier et validez :</h2>
<button id="initiateProcessingButton" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out disabled:opacity-50" disabled>
Afficher 1er Fichier et Ajuster Zone
</button>
<div id="canvasContainer" class="relative flex flex-col items-center mt-4">
<canvas id="imageCanvas" class="border border-gray-400 rounded-lg shadow-md max-w-full h-auto" style="display: none;"></canvas>
</div>
<div id="loader"></div>
<button id="applyAndDownloadButton" class="hidden mt-4 bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition duration-150 ease-in-out">
Appliquer à Tous et Télécharger
</button>
</section>
</main>
<footer class="mt-8 pt-4 border-t border-gray-200 flex justify-center">
<button id="resetAllButtonBottom" title="Réinitialiser tous les paramètres" class="p-2 text-gray-500 hover:text-blue-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw">
<path d="M3 2v6h6"/>
<path d="M3.31 15A9 9 0 0 0 20.7 7.5"/>
<path d="M21 22v-6h-6"/>
<path d="M20.7 9A9 9 0 0 0 3.31 16.5"/>
</svg>
</button>
</footer>
</div>
<script>
const { jsPDF } = window.jspdf;
// Références DOM
const allQuadrants = document.querySelectorAll('.quadrant');
const fileUpload = document.getElementById('fileUpload');
const outputFormatSelect = document.getElementById('outputFormatSelect');
const initiateProcessingButton = document.getElementById('initiateProcessingButton');
const imageCanvas = document.getElementById('imageCanvas');
const canvasContainer = document.getElementById('canvasContainer');
const applyAndDownloadButton = document.getElementById('applyAndDownloadButton');
const messageBox = document.getElementById('message-box');
const loader = document.getElementById('loader');
const resetAllButtonTop = document.getElementById('resetAllButtonTop');
const resetAllButtonBottom = document.getElementById('resetAllButtonBottom');
const ctx = imageCanvas.getContext('2d');
// État global
let selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
let allUploadedFiles = [];
let currentFileForAdjustment = null;
let currentSelectionRect = { x: 0, y: 0, w: 0, h: 0, isDefined: false };
let activeDragAction = null;
let dragStartCoords = { x: 0, y: 0 };
let rectStartCoords = { x: 0, y: 0, w: 0, h: 0 };
const HANDLE_SIZE = 14;
let handles = [];
// --- Constantes pour PDF A4 (en points, 1pt = 1/72 inch) ---
const A4_WIDTH_PT = 595.28;
const A4_HEIGHT_PT = 841.89;
function showMessage(text, type = 'info', duration = 4000) {
messageBox.textContent = text;
messageBox.className = '';
messageBox.classList.add(type);
messageBox.style.display = 'block';
setTimeout(() => { messageBox.style.display = 'none'; }, duration);
}
function checkInitiateButtonState() {
initiateProcessingButton.disabled = !(allUploadedFiles.length > 0 && selectedQuadrantInfo.quadrantIndex !== null);
}
allQuadrants.forEach(quadrant => {
quadrant.addEventListener('click', () => {
allQuadrants.forEach(q => q.classList.remove('selected'));
quadrant.classList.add('selected');
selectedQuadrantInfo.orientation = quadrant.dataset.orientation;
selectedQuadrantInfo.quadrantIndex = parseInt(quadrant.dataset.quadrant);
checkInitiateButtonState();
showMessage(`Zone initiale: ${selectedQuadrantInfo.orientation}, Q${selectedQuadrantInfo.quadrantIndex + 1}. Téléversez des fichiers.`, 'info');
});
});
fileUpload.addEventListener('change', (event) => {
if (event.target.files.length === 0) {
allUploadedFiles = [];
currentFileForAdjustment = null;
resetCanvasAndState(false);
checkInitiateButtonState();
return;
}
allUploadedFiles = Array.from(event.target.files);
currentFileForAdjustment = null;
resetCanvasAndState(false);
if (allUploadedFiles.length > 0) {
showMessage(`${allUploadedFiles.length} fichier(s) sélectionné(s). Prêt à ajuster le 1er.`, 'success');
}
checkInitiateButtonState();
});
async function loadFileForAdjustment(file) {
return new Promise((resolve, reject) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur chargement image pour ajustement.'));
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('Erreur lecture fichier image.'));
reader.readAsDataURL(file);
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
fileReader.onload = function() {
const typedarray = new Uint8Array(this.result);
pdfjsLib.getDocument(typedarray).promise.then(pdfDoc_ => {
pdfDoc_.getPage(1).then(page => {
const viewport = page.getViewport({ scale: 2.0 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.height = viewport.height;
tempCanvas.width = viewport.width;
page.render({ canvasContext: tempCtx, viewport: viewport }).promise.then(() => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error('Erreur conversion 1ère page PDF en image.'));
img.src = tempCanvas.toDataURL('image/png');
}).catch(reject);
}).catch(reject);
}).catch(reject);
};
fileReader.onerror = () => reject(new Error('Erreur lecture fichier PDF.'));
fileReader.readAsArrayBuffer(file);
} else {
reject(new Error('Type de fichier non supporté pour l\'ajustement.'));
}
});
}
initiateProcessingButton.addEventListener('click', async () => {
if (allUploadedFiles.length === 0 || selectedQuadrantInfo.quadrantIndex === null) {
showMessage('Sélectionnez zone ET téléversez au moins un fichier.', 'error');
return;
}
loader.style.display = 'block';
initiateProcessingButton.disabled = true;
try {
currentFileForAdjustment = await loadFileForAdjustment(allUploadedFiles[0]);
setupInteractiveStage(currentFileForAdjustment);
initiateProcessingButton.classList.add('hidden');
applyAndDownloadButton.classList.remove('hidden');
imageCanvas.style.display = 'block';
showMessage('Ajustez le rectangle sur ce 1er fichier, puis validez.', 'info');
} catch (error) {
showMessage(error.message || 'Erreur chargement du 1er fichier.', 'error');
console.error("Error loading first file for adjustment:", error);
} finally {
loader.style.display = 'none';
initiateProcessingButton.disabled = false;
}
});
function setupInteractiveStage(imageToAdjust) {
imageCanvas.width = imageToAdjust.width;
imageCanvas.height = imageToAdjust.height;
const imgW = imageToAdjust.width;
const imgH = imageToAdjust.height;
currentSelectionRect.w = imgW / 2;
currentSelectionRect.h = imgH / 2;
switch (selectedQuadrantInfo.quadrantIndex) {
case 0: currentSelectionRect.x = 0; currentSelectionRect.y = 0; break;
case 1: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = 0; break;
case 2: currentSelectionRect.x = 0; currentSelectionRect.y = imgH / 2; break;
case 3: currentSelectionRect.x = imgW / 2; currentSelectionRect.y = imgH / 2; break;
}
currentSelectionRect.isDefined = true;
createHandles();
drawCanvasWithSelectionAndHandles(imageToAdjust);
}
function drawCanvasWithSelectionAndHandles(imageToDraw, isFinal = false) {
if (!imageToDraw) return;
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
ctx.drawImage(imageToDraw, 0, 0, imageCanvas.width, imageCanvas.height);
if (currentSelectionRect.isDefined) {
if (isFinal) {
ctx.fillStyle = 'white';
ctx.fillRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
removeHandles();
} else {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.8)';
ctx.lineWidth = 2;
ctx.strokeRect(currentSelectionRect.x, currentSelectionRect.y, currentSelectionRect.w, currentSelectionRect.h);
updateHandlesPositions();
}
}
}
function createHandles() {
removeHandles();
const handleTypes = ['tl', 't', 'tr', 'l', 'r', 'bl', 'b', 'br'];
handleTypes.forEach(type => {
const handle = document.createElement('div');
handle.classList.add('handle');
handle.dataset.type = type;
canvasContainer.appendChild(handle);
handles.push(handle);
});
updateHandlesPositions();
}
function removeHandles() {
handles.forEach(h => h.remove());
handles = [];
}
function updateHandlesPositions() {
if (!currentSelectionRect.isDefined || handles.length === 0 || !imageCanvas.offsetParent) return;
const { x, y, w, h } = currentSelectionRect;
const canvasRect = imageCanvas.getBoundingClientRect();
const scaleX = canvasRect.width / imageCanvas.width;
const scaleY = canvasRect.height / imageCanvas.height;
const positions = {
'tl': { left: x * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
't': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'tr': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: y * scaleY - HANDLE_SIZE / 2 },
'l': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'r': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h / 2) * scaleY - HANDLE_SIZE / 2 },
'bl': { left: x * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'b': { left: (x + w / 2) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 },
'br': { left: (x + w) * scaleX - HANDLE_SIZE / 2, top: (y + h) * scaleY - HANDLE_SIZE / 2 }
};
handles.forEach(handle => {
const type = handle.dataset.type;
handle.style.left = `${canvasRect.left + positions[type].left - canvasContainer.getBoundingClientRect().left}px`;
handle.style.top = `${canvasRect.top + positions[type].top - canvasContainer.getBoundingClientRect().top}px`;
handle.style.cursor = getResizeCursor(type);
handle.style.display = 'block';
});
}
function getResizeCursor(handleType) {
switch (handleType) {
case 'tl': case 'br': return 'nwse-resize';
case 'tr': case 'bl': return 'nesw-resize';
case 't': case 'b': return 'ns-resize';
case 'l': case 'r': return 'ew-resize';
default: return 'default';
}
}
function getEventPosOnCanvas(event) {
const rect = imageCanvas.getBoundingClientRect();
let clientX, clientY;
if (event.touches && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return {
x: (clientX - rect.left) * (imageCanvas.width / rect.width),
y: (clientY - rect.top) * (imageCanvas.height / rect.height)
};
}
function handleInteractionStart(e) {
if (!currentSelectionRect.isDefined || !currentFileForAdjustment) return;
const target = e.target;
const eventPos = getEventPosOnCanvas(e);
if (target.classList.contains('handle')) {
activeDragAction = `resize-${target.dataset.type}`;
} else if (target === imageCanvas &&
eventPos.x >= currentSelectionRect.x && eventPos.x <= currentSelectionRect.x + currentSelectionRect.w &&
eventPos.y >= currentSelectionRect.y && eventPos.y <= currentSelectionRect.y + currentSelectionRect.h) {
activeDragAction = 'move';
} else {
return;
}
if (activeDragAction && e.cancelable) {
e.preventDefault();
}
dragStartCoords = eventPos;
rectStartCoords = { ...currentSelectionRect };
}
function handleInteractionMove(e) {
if (!activeDragAction || !currentSelectionRect.isDefined) {
if (e.type === 'mousemove' && (e.target === imageCanvas || e.target.classList.contains('handle'))) {
const mousePos = getEventPosOnCanvas(e);
let cursor = 'default';
if (currentSelectionRect.isDefined) {
for (const handle of handles) {
const handleRect = handle.getBoundingClientRect();
const canvasRect = imageCanvas.getBoundingClientRect();
const handleCanvasX = (handleRect.left - canvasRect.left + HANDLE_SIZE/2) * (imageCanvas.width / canvasRect.width);
const handleCanvasY = (handleRect.top - canvasRect.top + HANDLE_SIZE/2) * (imageCanvas.height / canvasRect.height);
if (Math.abs(mousePos.x - handleCanvasX) < HANDLE_SIZE && Math.abs(mousePos.y - handleCanvasY) < HANDLE_SIZE * 1.5) {
cursor = getResizeCursor(handle.dataset.type);
break;
}
}
if (cursor === 'default' &&
mousePos.x >= currentSelectionRect.x && mousePos.x <= currentSelectionRect.x + currentSelectionRect.w &&
mousePos.y >= currentSelectionRect.y && mousePos.y <= currentSelectionRect.y + currentSelectionRect.h) {
cursor = 'move';
}
}
imageCanvas.style.cursor = cursor;
}
return;
}
if (e.cancelable) {
e.preventDefault();
}
const eventPos = getEventPosOnCanvas(e);
const deltaX = eventPos.x - dragStartCoords.x;
const deltaY = eventPos.y - dragStartCoords.y;
let { x, y, w, h } = rectStartCoords;
if (activeDragAction === 'move') {
x += deltaX;
y += deltaY;
} else if (activeDragAction.startsWith('resize-')) {
const type = activeDragAction.split('-')[1];
if (type.includes('l')) { x += deltaX; w -= deltaX; }
if (type.includes('r')) { w += deltaX; }
if (type.includes('t')) { y += deltaY; h -= deltaY; }
if (type.includes('b')) { h += deltaY; }
}
const minSize = 20;
if (w < minSize) {
if (activeDragAction.includes('l') || activeDragAction.includes('tl') || activeDragAction.includes('bl')) x = rectStartCoords.x + rectStartCoords.w - minSize;
w = minSize;
}
if (h < minSize) {
if (activeDragAction.includes('t') || activeDragAction.includes('tl') || activeDragAction.includes('tr')) y = rectStartCoords.y + rectStartCoords.h - minSize;
h = minSize;
}
x = Math.max(0, Math.min(x, imageCanvas.width - w));
y = Math.max(0, Math.min(y, imageCanvas.height - h));
w = Math.min(w, imageCanvas.width - x);
h = Math.min(h, imageCanvas.height - y);
currentSelectionRect = { x, y, w, h, isDefined: true };
drawCanvasWithSelectionAndHandles(currentFileForAdjustment);
}
function handleInteractionEnd(e) {
if (activeDragAction) {
activeDragAction = null;
imageCanvas.style.cursor = 'default';
updateHandlesPositions();
}
}
canvasContainer.addEventListener('mousedown', handleInteractionStart);
document.addEventListener('mousemove', handleInteractionMove);
document.addEventListener('mouseup', handleInteractionEnd);
canvasContainer.addEventListener('touchstart', handleInteractionStart, { passive: false });
document.addEventListener('touchmove', handleInteractionMove, { passive: false });
document.addEventListener('touchend', handleInteractionEnd);
applyAndDownloadButton.addEventListener('click', async () => {
if (!currentSelectionRect.isDefined || allUploadedFiles.length === 0) {
showMessage('Zone non définie ou aucun fichier chargé.', 'error');
return;
}
loader.style.display = 'block';
applyAndDownloadButton.disabled = true;
let filesProcessedCount = 0;
const chosenOutputFormat = outputFormatSelect.value;
for (let i = 0; i < allUploadedFiles.length; i++) {
const file = allUploadedFiles[i];
const originalFilename = file.name.substring(0, file.name.lastIndexOf('.')) || file.name;
showMessage(`Traitement de ${file.name} (${i+1}/${allUploadedFiles.length})...`, 'info', 60000);
try {
let effectiveOutputFormat = chosenOutputFormat;
if (chosenOutputFormat === 'source') {
effectiveOutputFormat = file.type.startsWith('image/') ? (file.type === 'image/jpeg' ? 'image/jpeg' : 'image/png') : 'application/pdf';
}
if (file.type.startsWith('image/')) {
const img = await loadFileForAdjustment(file);
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = img.width;
tempCanvas.height = img.height;
tempCtx.drawImage(img, 0, 0);
tempCtx.fillStyle = 'white';
const scaleX = img.width / currentFileForAdjustment.width;
const scaleY = img.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleX,
currentSelectionRect.y * scaleY,
currentSelectionRect.w * scaleX,
currentSelectionRect.h * scaleY);
if (effectiveOutputFormat === 'application/pdf') {
const pdfOutput = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' });
const pageDataUrl = tempCanvas.toDataURL('image/png');
let pageOrientation = img.width > img.height ? 'l' : 'p';
let targetWidthPt = pageOrientation === 'l' ? A4_HEIGHT_PT : A4_WIDTH_PT;
let targetHeightPt = pageOrientation === 'l' ? A4_WIDTH_PT : A4_HEIGHT_PT;
pdfOutput.internal.pageSize.width = targetWidthPt;
pdfOutput.internal.pageSize.height = targetHeightPt;
const ratio = Math.min(targetWidthPt / img.width, targetHeightPt / img.height);
const scaledWidth = img.width * ratio;
const scaledHeight = img.height * ratio;
const xOffset = (targetWidthPt - scaledWidth) / 2;
const yOffset = (targetHeightPt - scaledHeight) / 2;
pdfOutput.addImage(pageDataUrl, 'PNG', xOffset, yOffset, scaledWidth, scaledHeight);
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else {
const extension = effectiveOutputFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(effectiveOutputFormat, effectiveOutputFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_modifie.${extension}`);
}
filesProcessedCount++;
} else if (file.type === 'application/pdf') {
const fileReader = new FileReader();
const arrayBuffer = await new Promise((resolve, reject) => {
fileReader.onload = () => resolve(fileReader.result);
fileReader.onerror = reject;
fileReader.readAsArrayBuffer(file);
});
const pdfDocInstance = await pdfjsLib.getDocument(new Uint8Array(arrayBuffer)).promise;
const numPages = pdfDocInstance.numPages;
if (effectiveOutputFormat === 'image/png' || effectiveOutputFormat === 'image/jpeg') {
const page = await pdfDocInstance.getPage(1);
const viewport = page.getViewport({ scale: 2.0 });
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
const scaleToPageX = viewport.width / currentFileForAdjustment.width;
const scaleToPageY = viewport.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleToPageX,
currentSelectionRect.y * scaleToPageY,
currentSelectionRect.w * scaleToPageX,
currentSelectionRect.h * scaleToPageY);
const extension = effectiveOutputFormat === 'image/jpeg' ? 'jpg' : 'png';
const dataURL = tempCanvas.toDataURL(effectiveOutputFormat, effectiveOutputFormat === 'image/jpeg' ? 0.9 : undefined);
triggerDownload(dataURL, `${originalFilename}_page1_modifie.${extension}`);
} else { // PDF to PDF
let pdfOutput;
for (let j = 1; j <= numPages; j++) {
const page = await pdfDocInstance.getPage(j);
const viewport = page.getViewport({ scale: 2.0 });
let pageOrientation = viewport.width > viewport.height ? 'l' : 'p';
let pageTargetWidthPt = pageOrientation === 'l' ? A4_HEIGHT_PT : A4_WIDTH_PT;
let pageTargetHeightPt = pageOrientation === 'l' ? A4_WIDTH_PT : A4_HEIGHT_PT;
if (j === 1) {
pdfOutput = new jsPDF({ orientation: pageOrientation, unit: 'pt', format: [pageTargetWidthPt, pageTargetHeightPt] });
} else {
pdfOutput.addPage([pageTargetWidthPt, pageTargetHeightPt], pageOrientation);
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = viewport.width;
tempCanvas.height = viewport.height;
await page.render({ canvasContext: tempCtx, viewport: viewport }).promise;
tempCtx.fillStyle = 'white';
const scaleToPageX = viewport.width / currentFileForAdjustment.width;
const scaleToPageY = viewport.height / currentFileForAdjustment.height;
tempCtx.fillRect(currentSelectionRect.x * scaleToPageX,
currentSelectionRect.y * scaleToPageY,
currentSelectionRect.w * scaleToPageX,
currentSelectionRect.h * scaleToPageY);
const pageDataUrl = tempCanvas.toDataURL('image/png');
const ratio = Math.min(pageTargetWidthPt / viewport.width, pageTargetHeightPt / viewport.height);
const scaledWidth = viewport.width * ratio;
const scaledHeight = viewport.height * ratio;
const xOffset = (pageTargetWidthPt - scaledWidth) / 2;
const yOffset = (pageTargetHeightPt - scaledHeight) / 2;
pdfOutput.addImage(pageDataUrl, 'PNG', xOffset, yOffset, scaledWidth, scaledHeight);
}
if (pdfOutput) {
pdfOutput.save(`${originalFilename}_modifie.pdf`);
} else if (numPages > 0) {
console.error("pdfOutput was not initialized for PDF:", file.name);
showMessage(`Erreur interne lors de la création du PDF pour ${file.name}`, 'error');
}
}
filesProcessedCount++;
}
await new Promise(resolve => setTimeout(resolve, 200));
} catch (err) {
console.error("Erreur traitement fichier:", file.name, err);
showMessage(`Erreur avec ${file.name}: ${err.message || 'Inconnue'}`, 'error');
}
}
loader.style.display = 'none';
applyAndDownloadButton.disabled = false;
if (filesProcessedCount > 0) {
showMessage(`${filesProcessedCount} fichier(s) traité(s) et téléchargé(s).`, 'success');
} else if (allUploadedFiles.length > 0) {
showMessage(`Aucun fichier n'a pu être traité avec succès.`, 'error');
}
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
});
function triggerDownload(dataURL, filename) {
const link = document.createElement('a');
link.href = dataURL;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
resetAllButtonTop.addEventListener('click', () => {
resetApplicationState();
showMessage('Tous les paramètres ont été réinitialisés.', 'info');
});
resetAllButtonBottom.addEventListener('click', () => {
resetApplicationState();
showMessage('Tous les paramètres ont été réinitialisés.', 'info');
});
function resetApplicationState() {
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
allUploadedFiles = [];
fileUpload.value = '';
resetCanvasAndState(true);
outputFormatSelect.value = 'source';
initiateProcessingButton.disabled = true;
initiateProcessingButton.classList.remove('hidden');
applyAndDownloadButton.classList.add('hidden');
loader.style.display = 'none';
messageBox.style.display = 'none';
}
function resetCanvasAndState(resetQuadrantSelectionInternal = true) {
ctx.clearRect(0, 0, imageCanvas.width, imageCanvas.height);
imageCanvas.style.display = 'none';
removeHandles();
currentFileForAdjustment = null;
currentSelectionRect.isDefined = false;
activeDragAction = null;
if (resetQuadrantSelectionInternal) {
allQuadrants.forEach(q => q.classList.remove('selected'));
selectedQuadrantInfo = { orientation: null, quadrantIndex: null };
}
checkInitiateButtonState();
}
checkInitiateButtonState();
window.addEventListener('resize', () => {
if (currentSelectionRect.isDefined && handles.length > 0 && imageCanvas.offsetParent) {
updateHandlesPositions();
}
});
</script>
</body>
</html>