From 03991517b25da24b1e26423f8ac00b04bb65d1b2 Mon Sep 17 00:00:00 2001 From: killercow Date: Mon, 12 Jan 2026 14:02:59 +0000 Subject: [PATCH] Compress photos for email to fit mailto limits - Added compressImageForEmail function (300x300px, quality 0.3) - Photos in email are now ~5-15KB instead of ~50-200KB - sendEmail now async to handle photo compression - Updated email text to indicate photos are resized - Original quality preserved in CSV export - Reduced mailto limit check to 64KB --- examples/inventory_corporate.html | 79 +++++++++++++++++++++++-------- examples/inventory_minimal.html | 79 +++++++++++++++++++++++-------- examples/inventory_modern.html | 79 +++++++++++++++++++++++-------- src/templates.py | 79 +++++++++++++++++++++++-------- 4 files changed, 240 insertions(+), 76 deletions(-) diff --git a/examples/inventory_corporate.html b/examples/inventory_corporate.html index 5496187..ed0e692 100644 --- a/examples/inventory_corporate.html +++ b/examples/inventory_corporate.html @@ -1203,6 +1203,40 @@ input.invalid, select.invalid, textarea.invalid { reader.readAsDataURL(file); } + // Compress image for email (much smaller) + function compressImageForEmail(dataUrl, callback) { + const img = new Image(); + img.onload = function() { + const canvas = document.createElement('canvas'); + const maxSize = 300; // Max 300x300 for email + let width = img.width; + let height = img.height; + + // Scale down to max 300px + if (width > height) { + if (width > maxSize) { + height = height * (maxSize / width); + width = maxSize; + } + } else { + if (height > maxSize) { + width = width * (maxSize / height); + height = maxSize; + } + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + // Very low quality for email + callback(canvas.toDataURL('image/jpeg', 0.3)); + }; + img.src = dataUrl; + } + // Remove photo window.removePhoto = function(fieldId) { const preview = document.getElementById(fieldId); @@ -1262,7 +1296,7 @@ input.invalid, select.invalid, textarea.invalid { } // Use setTimeout to allow UI to update before heavy processing - setTimeout(function() { + setTimeout(async function() { const data = getFormData(); // Build subject @@ -1283,8 +1317,9 @@ input.invalid, select.invalid, textarea.invalid { body += 'Versie: ' + CONFIG.version + '\n'; body += 'Datum: ' + formatDateTime(new Date()) + '\n\n'; - // Collect photos separately + // Collect photos separately and compress them const photos = []; + const photoPromises = []; Object.entries(data).forEach(([key, value]) => { if (value && value !== '') { @@ -1292,14 +1327,18 @@ input.invalid, select.invalid, textarea.invalid { if (typeof value === 'string' && value.startsWith('data:image/')) { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; - // Extract extension from data URL (e.g., data:image/jpeg;base64,...) - const mimeMatch = value.match(/data:image\/([a-z]+);/); - const ext = mimeMatch ? mimeMatch[1] : 'jpg'; - photos.push({ - name: labelText, - extension: ext, - data: value + + // Compress photo for email + const promise = new Promise((resolve) => { + compressImageForEmail(value, function(compressedData) { + resolve({ + name: labelText, + extension: 'jpg', + data: compressedData + }); + }); }); + photoPromises.push(promise); } else { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; @@ -1309,17 +1348,19 @@ input.invalid, select.invalid, textarea.invalid { } }); + // Wait for all photos to be compressed + const compressedPhotos = await Promise.all(photoPromises); + body += '\n========================\n'; // Add photos section only if there are photos - if (photos.length > 0) { - body += '\nFOTO BIJLAGEN (BASE64)\n'; - body += '========================\n'; - body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n'; - body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\n'; - body += 'of gebruik een online tool zoals base64-image.de\n\n'; + if (compressedPhotos.length > 0) { + body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n'; + body += '==============================================\n'; + body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n'; + body += 'Voor originele kwaliteit, gebruik CSV export.\n\n'; - photos.forEach((photo, index) => { + compressedPhotos.forEach((photo, index) => { const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension; body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n'; body += '>>> START BASE64 >>>\n'; @@ -1338,9 +1379,9 @@ input.invalid, select.invalid, textarea.invalid { button.disabled = false; } - // Check if mailto URL is too long (most clients support ~2000 chars) - if (mailto.length > 100000) { - showToast('Email te groot door foto. Gebruik CSV export.', 'error'); + // Check if mailto URL is too long + if (mailto.length > 64000) { + showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error'); return; } diff --git a/examples/inventory_minimal.html b/examples/inventory_minimal.html index 488ce79..e0e6af6 100644 --- a/examples/inventory_minimal.html +++ b/examples/inventory_minimal.html @@ -1234,6 +1234,40 @@ select { reader.readAsDataURL(file); } + // Compress image for email (much smaller) + function compressImageForEmail(dataUrl, callback) { + const img = new Image(); + img.onload = function() { + const canvas = document.createElement('canvas'); + const maxSize = 300; // Max 300x300 for email + let width = img.width; + let height = img.height; + + // Scale down to max 300px + if (width > height) { + if (width > maxSize) { + height = height * (maxSize / width); + width = maxSize; + } + } else { + if (height > maxSize) { + width = width * (maxSize / height); + height = maxSize; + } + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + // Very low quality for email + callback(canvas.toDataURL('image/jpeg', 0.3)); + }; + img.src = dataUrl; + } + // Remove photo window.removePhoto = function(fieldId) { const preview = document.getElementById(fieldId); @@ -1293,7 +1327,7 @@ select { } // Use setTimeout to allow UI to update before heavy processing - setTimeout(function() { + setTimeout(async function() { const data = getFormData(); // Build subject @@ -1314,8 +1348,9 @@ select { body += 'Versie: ' + CONFIG.version + '\n'; body += 'Datum: ' + formatDateTime(new Date()) + '\n\n'; - // Collect photos separately + // Collect photos separately and compress them const photos = []; + const photoPromises = []; Object.entries(data).forEach(([key, value]) => { if (value && value !== '') { @@ -1323,14 +1358,18 @@ select { if (typeof value === 'string' && value.startsWith('data:image/')) { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; - // Extract extension from data URL (e.g., data:image/jpeg;base64,...) - const mimeMatch = value.match(/data:image\/([a-z]+);/); - const ext = mimeMatch ? mimeMatch[1] : 'jpg'; - photos.push({ - name: labelText, - extension: ext, - data: value + + // Compress photo for email + const promise = new Promise((resolve) => { + compressImageForEmail(value, function(compressedData) { + resolve({ + name: labelText, + extension: 'jpg', + data: compressedData + }); + }); }); + photoPromises.push(promise); } else { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; @@ -1340,17 +1379,19 @@ select { } }); + // Wait for all photos to be compressed + const compressedPhotos = await Promise.all(photoPromises); + body += '\n========================\n'; // Add photos section only if there are photos - if (photos.length > 0) { - body += '\nFOTO BIJLAGEN (BASE64)\n'; - body += '========================\n'; - body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n'; - body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\n'; - body += 'of gebruik een online tool zoals base64-image.de\n\n'; + if (compressedPhotos.length > 0) { + body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n'; + body += '==============================================\n'; + body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n'; + body += 'Voor originele kwaliteit, gebruik CSV export.\n\n'; - photos.forEach((photo, index) => { + compressedPhotos.forEach((photo, index) => { const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension; body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n'; body += '>>> START BASE64 >>>\n'; @@ -1369,9 +1410,9 @@ select { button.disabled = false; } - // Check if mailto URL is too long (most clients support ~2000 chars) - if (mailto.length > 100000) { - showToast('Email te groot door foto. Gebruik CSV export.', 'error'); + // Check if mailto URL is too long + if (mailto.length > 64000) { + showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error'); return; } diff --git a/examples/inventory_modern.html b/examples/inventory_modern.html index c7ac6fb..20c76ef 100644 --- a/examples/inventory_modern.html +++ b/examples/inventory_modern.html @@ -1265,6 +1265,40 @@ input.invalid, select.invalid, textarea.invalid { reader.readAsDataURL(file); } + // Compress image for email (much smaller) + function compressImageForEmail(dataUrl, callback) { + const img = new Image(); + img.onload = function() { + const canvas = document.createElement('canvas'); + const maxSize = 300; // Max 300x300 for email + let width = img.width; + let height = img.height; + + // Scale down to max 300px + if (width > height) { + if (width > maxSize) { + height = height * (maxSize / width); + width = maxSize; + } + } else { + if (height > maxSize) { + width = width * (maxSize / height); + height = maxSize; + } + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + // Very low quality for email + callback(canvas.toDataURL('image/jpeg', 0.3)); + }; + img.src = dataUrl; + } + // Remove photo window.removePhoto = function(fieldId) { const preview = document.getElementById(fieldId); @@ -1324,7 +1358,7 @@ input.invalid, select.invalid, textarea.invalid { } // Use setTimeout to allow UI to update before heavy processing - setTimeout(function() { + setTimeout(async function() { const data = getFormData(); // Build subject @@ -1345,8 +1379,9 @@ input.invalid, select.invalid, textarea.invalid { body += 'Versie: ' + CONFIG.version + '\n'; body += 'Datum: ' + formatDateTime(new Date()) + '\n\n'; - // Collect photos separately + // Collect photos separately and compress them const photos = []; + const photoPromises = []; Object.entries(data).forEach(([key, value]) => { if (value && value !== '') { @@ -1354,14 +1389,18 @@ input.invalid, select.invalid, textarea.invalid { if (typeof value === 'string' && value.startsWith('data:image/')) { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; - // Extract extension from data URL (e.g., data:image/jpeg;base64,...) - const mimeMatch = value.match(/data:image\/([a-z]+);/); - const ext = mimeMatch ? mimeMatch[1] : 'jpg'; - photos.push({ - name: labelText, - extension: ext, - data: value + + // Compress photo for email + const promise = new Promise((resolve) => { + compressImageForEmail(value, function(compressedData) { + resolve({ + name: labelText, + extension: 'jpg', + data: compressedData + }); + }); }); + photoPromises.push(promise); } else { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; @@ -1371,17 +1410,19 @@ input.invalid, select.invalid, textarea.invalid { } }); + // Wait for all photos to be compressed + const compressedPhotos = await Promise.all(photoPromises); + body += '\n========================\n'; // Add photos section only if there are photos - if (photos.length > 0) { - body += '\nFOTO BIJLAGEN (BASE64)\n'; - body += '========================\n'; - body += 'Onderstaande foto\'s zijn gecodeerd in base64 formaat.\n'; - body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\n'; - body += 'of gebruik een online tool zoals base64-image.de\n\n'; + if (compressedPhotos.length > 0) { + body += '\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\n'; + body += '==============================================\n'; + body += 'Foto\'s zijn verkleind naar 300x300px voor email.\n'; + body += 'Voor originele kwaliteit, gebruik CSV export.\n\n'; - photos.forEach((photo, index) => { + compressedPhotos.forEach((photo, index) => { const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension; body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\n'; body += '>>> START BASE64 >>>\n'; @@ -1400,9 +1441,9 @@ input.invalid, select.invalid, textarea.invalid { button.disabled = false; } - // Check if mailto URL is too long (most clients support ~2000 chars) - if (mailto.length > 100000) { - showToast('Email te groot door foto. Gebruik CSV export.', 'error'); + // Check if mailto URL is too long + if (mailto.length > 64000) { + showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error'); return; } diff --git a/src/templates.py b/src/templates.py index 86bdc42..742c3fc 100644 --- a/src/templates.py +++ b/src/templates.py @@ -1469,6 +1469,40 @@ JAVASCRIPT = """ reader.readAsDataURL(file); } + // Compress image for email (much smaller) + function compressImageForEmail(dataUrl, callback) { + const img = new Image(); + img.onload = function() { + const canvas = document.createElement('canvas'); + const maxSize = 300; // Max 300x300 for email + let width = img.width; + let height = img.height; + + // Scale down to max 300px + if (width > height) { + if (width > maxSize) { + height = height * (maxSize / width); + width = maxSize; + } + } else { + if (height > maxSize) { + width = width * (maxSize / height); + height = maxSize; + } + } + + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + // Very low quality for email + callback(canvas.toDataURL('image/jpeg', 0.3)); + }; + img.src = dataUrl; + } + // Remove photo window.removePhoto = function(fieldId) { const preview = document.getElementById(fieldId); @@ -1528,7 +1562,7 @@ JAVASCRIPT = """ } // Use setTimeout to allow UI to update before heavy processing - setTimeout(function() { + setTimeout(async function() { const data = getFormData(); // Build subject @@ -1549,8 +1583,9 @@ JAVASCRIPT = """ body += 'Versie: ' + CONFIG.version + '\\n'; body += 'Datum: ' + formatDateTime(new Date()) + '\\n\\n'; - // Collect photos separately + // Collect photos separately and compress them const photos = []; + const photoPromises = []; Object.entries(data).forEach(([key, value]) => { if (value && value !== '') { @@ -1558,14 +1593,18 @@ JAVASCRIPT = """ if (typeof value === 'string' && value.startsWith('data:image/')) { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; - // Extract extension from data URL (e.g., data:image/jpeg;base64,...) - const mimeMatch = value.match(/data:image\\/([a-z]+);/); - const ext = mimeMatch ? mimeMatch[1] : 'jpg'; - photos.push({ - name: labelText, - extension: ext, - data: value + + // Compress photo for email + const promise = new Promise((resolve) => { + compressImageForEmail(value, function(compressedData) { + resolve({ + name: labelText, + extension: 'jpg', + data: compressedData + }); + }); }); + photoPromises.push(promise); } else { const label = document.querySelector(`label[for="${key}"]`); const labelText = label ? label.textContent.replace('*', '').trim() : key; @@ -1575,17 +1614,19 @@ JAVASCRIPT = """ } }); + // Wait for all photos to be compressed + const compressedPhotos = await Promise.all(photoPromises); + body += '\\n========================\\n'; // Add photos section only if there are photos - if (photos.length > 0) { - body += '\\nFOTO BIJLAGEN (BASE64)\\n'; - body += '========================\\n'; - body += 'Onderstaande foto\\'s zijn gecodeerd in base64 formaat.\\n'; - body += 'Kopieer de tekst tussen START en EINDE naar een base64 decoder\\n'; - body += 'of gebruik een online tool zoals base64-image.de\\n\\n'; + if (compressedPhotos.length > 0) { + body += '\\nFOTO BIJLAGEN (BASE64 - verkleind voor email)\\n'; + body += '==============================================\\n'; + body += 'Foto\\'s zijn verkleind naar 300x300px voor email.\\n'; + body += 'Voor originele kwaliteit, gebruik CSV export.\\n\\n'; - photos.forEach((photo, index) => { + compressedPhotos.forEach((photo, index) => { const filename = photo.name.replace(/[^a-zA-Z0-9]/g, '_') + '.' + photo.extension; body += '--- FOTO ' + (index + 1) + ': ' + filename + ' ---\\n'; body += '>>> START BASE64 >>>\\n'; @@ -1604,9 +1645,9 @@ JAVASCRIPT = """ button.disabled = false; } - // Check if mailto URL is too long (most clients support ~2000 chars) - if (mailto.length > 100000) { - showToast('Email te groot door foto. Gebruik CSV export.', 'error'); + // Check if mailto URL is too long + if (mailto.length > 64000) { + showToast('Email nog steeds te groot. Verklein foto of gebruik CSV.', 'error'); return; }