Verkleinerte Fotos heißen .jpg, Transparenz wird weiß statt schwarz

Ist eine Maximalgröße eingestellt, ersetzte das im Browser verkleinerte JPEG den Inhalt in der
Queue, der Name blieb aber z. B. „.avif"/„.webp" — der Server lehnte das ab. replaceQueuedBlob()
zieht die Endung jetzt auf .jpg nach (Galerie, Teilen, Kamera). Beim Verkleinern wird vor dem
Zeichnen weiß gefüllt, sonst wurden transparente Flächen im JPEG schwarz.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-10-19 22:09:35 +02:00
parent 0d2f485ecc
commit 91f3f483b5
2 changed files with 154 additions and 4 deletions

153
app.js
View file

@ -1320,7 +1320,11 @@ async function resizeImage(file, maxSide, timeoutMs, quality) {
const c = document.createElement('canvas'); const c = document.createElement('canvas');
c.width = Math.round(img.width * scale); c.width = Math.round(img.width * scale);
c.height = Math.round(img.height * scale); c.height = Math.round(img.height * scale);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height); const ctx = c.getContext('2d');
// Weisser Grund: JPEG kennt keine Transparenz, ein freigestelltes PNG/WebP wuerde sonst schwarz
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(img, 0, 0, c.width, c.height);
c.toBlob(b => resolve(b || file), 'image/jpeg', typeof quality === 'number' ? quality : 0.85); c.toBlob(b => resolve(b || file), 'image/jpeg', typeof quality === 'number' ? quality : 0.85);
}; };
img.onerror = () => { URL.revokeObjectURL(url); resolve(file); }; img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
@ -3613,11 +3617,14 @@ async function openCameraModal(orderId) {
<div class="fs-header"> <div class="fs-header">
<button class="icon-btn" id="cam-close">✕</button> <button class="icon-btn" id="cam-close">✕</button>
<div class="fs-title">📷 Kamera</div> <div class="fs-title">📷 Kamera</div>
<button class="icon-btn cam-blitz" id="cam-blitz" title="Blitz" hidden></button>
<button class="icon-btn" id="cam-flip" title="Kamera wechseln">🔄</button> <button class="icon-btn" id="cam-flip" title="Kamera wechseln">🔄</button>
</div> </div>
<div class="cam-stage"> <div class="cam-stage" id="cam-stage">
<video id="cam-video" playsinline autoplay muted></video> <video id="cam-video" playsinline autoplay muted></video>
<div class="cam-hint" id="cam-hint">Kamera wird gestartet…</div> <div class="cam-hint" id="cam-hint">Kamera wird gestartet…</div>
<div class="cam-fokus-ring" id="cam-fokus-ring" hidden></div>
<button class="cam-fokus-fest" id="cam-fokus-fest" hidden>🎯 Fokus fest · zurück auf Auto</button>
</div> </div>
<div class="cam-filmstrip" id="cam-strip"></div> <div class="cam-filmstrip" id="cam-strip"></div>
<div class="cam-controls"> <div class="cam-controls">
@ -3633,6 +3640,10 @@ async function openCameraModal(orderId) {
const hint = modal.querySelector('#cam-hint'); const hint = modal.querySelector('#cam-hint');
const countEl = modal.querySelector('#cam-count'); const countEl = modal.querySelector('#cam-count');
const shutter = modal.querySelector('#cam-shutter'); const shutter = modal.querySelector('#cam-shutter');
const stage = modal.querySelector('#cam-stage');
const blitzBtn = modal.querySelector('#cam-blitz');
const fokusRing = modal.querySelector('#cam-fokus-ring');
const fokusFest = modal.querySelector('#cam-fokus-fest');
let stream = null; let stream = null;
let facing = 'environment'; let facing = 'environment';
@ -3705,6 +3716,7 @@ async function openCameraModal(orderId) {
video.srcObject = stream; video.srcObject = stream;
await video.play().catch(() => {}); await video.play().catch(() => {});
hint.style.display = 'none'; hint.style.display = 'none';
await kameraExtras(my);
} catch (err) { } catch (err) {
if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant if (my !== gen || !document.body.contains(modal)) return; // abgebrochen, nicht mehr relevant
console.warn('[Kamera]', err); console.warn('[Kamera]', err);
@ -3763,6 +3775,132 @@ async function openCameraModal(orderId) {
} }
} }
/* Blitz und Fokus (Eddy, 19.10.2026: „den Blitz kann man nicht einstellen und den Fokus auch nicht").
*
* Angeboten wird nur, was das Geraet meldet — Chrome kennt es erst, wenn der Strom laeuft:
* - Blitz beim Ausloesen: ImageCapture.getPhotoCapabilities().fillLightMode (auto/flash),
* wirkt nur ueber takePhoto({ fillLightMode }) — nicht beim Standbild aus dem Sucher.
* - Licht (Dauerlicht): track.getCapabilities().torch, per applyConstraints.
* - Scharfstellen per Antippen: focusMode 'single-shot' (stellt einmal scharf und haelt den
* Fokus fest) bzw. 'continuous', jeweils mit pointsOfInterest = angetippte Stelle (0..1).
* Frontkamera, Webcam am Tablet (X12) usw. melden meist nichts davon → kein Knopf, kein Antippen.
*/
const BLITZ_TEXT = { off: '⚡ Aus', auto: '⚡ Auto', flash: '⚡ An', torch: '🔦 Licht' };
const BLITZ_KEY = 'baustelle.kamera.blitz';
let imageCap = null;
let blitzModi = [];
let blitz = 'off';
let fotoBlitz = false; // kann takePhoto() blitzen (fillLightMode)?
let fokusModus = ''; // 'single-shot' | 'continuous' | '' (kein Antippen)
let tippHinweis = false;
function videoTrack() {
return stream ? stream.getVideoTracks()[0] || null : null;
}
async function kameraExtras(my) {
imageCap = null; blitzModi = []; fotoBlitz = false; fokusModus = '';
blitzBtn.hidden = true; fokusFest.hidden = true; fokusRing.hidden = true;
const track = videoTrack();
if (!track) return;
let caps = {};
try { caps = typeof track.getCapabilities === 'function' ? (track.getCapabilities() || {}) : {}; } catch (_) {}
let licht = [];
if (typeof ImageCapture === 'function') {
try {
imageCap = new ImageCapture(track);
const pc = await imageCap.getPhotoCapabilities();
licht = Array.isArray(pc && pc.fillLightMode) ? pc.fillLightMode : [];
} catch (_) { /* kein Standbild-Erfasser — Sucherbild bleibt */ }
}
if (my !== gen) return; // inzwischen geschlossen/gewechselt
fotoBlitz = licht.includes('auto') || licht.includes('flash');
const modi = [];
if (fotoBlitz || caps.torch) modi.push('off');
if (licht.includes('auto')) modi.push('auto');
if (licht.includes('flash')) modi.push('flash');
if (caps.torch) modi.push('torch');
blitzModi = modi;
let gemerkt = 'off';
try { gemerkt = localStorage.getItem(BLITZ_KEY) || 'off'; } catch (_) {}
blitz = modi.includes(gemerkt) ? gemerkt : 'off';
if (modi.length > 1) {
blitzBtn.hidden = false;
blitzBtn.textContent = BLITZ_TEXT[blitz];
if (blitz === 'torch') lichtSchalten(true);
}
const fm = Array.isArray(caps.focusMode) ? caps.focusMode : [];
fokusModus = fm.includes('single-shot') ? 'single-shot' : (fm.includes('continuous') ? 'continuous' : '');
if (fokusModus && !tippHinweis) {
tippHinweis = true;
hint.textContent = 'Antippen stellt dort scharf';
hint.style.display = '';
setTimeout(() => { if (hint.textContent === 'Antippen stellt dort scharf') hint.style.display = 'none'; }, 2500);
}
}
async function lichtSchalten(an) {
const track = videoTrack();
if (!track) return false;
try { await track.applyConstraints({ advanced: [{ torch: an }] }); return true; }
catch (_) { return false; }
}
blitzBtn.onclick = async () => {
if (blitzModi.length < 2) return;
const vorher = blitz;
blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length];
if (vorher === 'torch' || blitz === 'torch') {
if (!await lichtSchalten(blitz === 'torch') && blitz === 'torch') {
showToast('Das Licht lässt sich hier nicht schalten', 'warn');
blitz = blitzModi[(blitzModi.indexOf(blitz) + 1) % blitzModi.length];
}
}
blitzBtn.textContent = BLITZ_TEXT[blitz];
try { localStorage.setItem(BLITZ_KEY, blitz); } catch (_) {}
};
stage.addEventListener('click', async (ev) => {
const track = videoTrack();
if (!track || !fokusModus || !video.videoWidth || ev.target === fokusFest) return;
// Antippstelle in Bildkoordinaten 0..1 — die Vorschau ist eingepasst (object-fit: contain)
const r = video.getBoundingClientRect();
const sk = Math.min(r.width / video.videoWidth, r.height / video.videoHeight);
const w = video.videoWidth * sk, h = video.videoHeight * sk;
const x = (ev.clientX - r.left - (r.width - w) / 2) / w;
const y = (ev.clientY - r.top - (r.height - h) / 2) / h;
if (x < 0 || x > 1 || y < 0 || y > 1) return;
const sr = stage.getBoundingClientRect();
fokusRing.style.left = (ev.clientX - sr.left) + 'px';
fokusRing.style.top = (ev.clientY - sr.top) + 'px';
fokusRing.hidden = false;
fokusRing.classList.remove('ok', 'aktiv');
void fokusRing.offsetWidth; // Animation neu starten
fokusRing.classList.add('aktiv');
let ok = false;
try {
await track.applyConstraints({ advanced: [{ pointsOfInterest: [{ x, y }], focusMode: fokusModus }] });
ok = true;
} catch (_) {
// Ohne Punktwahl wenigstens neu scharfstellen
try { await track.applyConstraints({ advanced: [{ focusMode: fokusModus }] }); ok = true; } catch (_) {}
}
fokusRing.classList.toggle('ok', ok);
setTimeout(() => { fokusRing.hidden = true; }, 900);
if (ok && fokusModus === 'single-shot') fokusFest.hidden = false;
});
fokusFest.onclick = async (ev) => {
ev.stopPropagation();
const track = videoTrack();
fokusFest.hidden = true;
if (track) { try { await track.applyConstraints({ advanced: [{ focusMode: 'continuous' }] }); } catch (_) {} }
};
/* Echtes Kamerafoto in voller Sensoraufloesung. /* Echtes Kamerafoto in voller Sensoraufloesung.
* *
* Ein Standbild aus dem Video-Stream hat hoechstens Stream-Aufloesung (hier bis 4K * Ein Standbild aus dem Video-Stream hat hoechstens Stream-Aufloesung (hier bis 4K
@ -3777,8 +3915,15 @@ async function openCameraModal(orderId) {
if (typeof ImageCapture === 'undefined' || !stream) return null; if (typeof ImageCapture === 'undefined' || !stream) return null;
const track = stream.getVideoTracks()[0]; const track = stream.getVideoTracks()[0];
if (!track || typeof track.getCapabilities !== 'function') return null; if (!track || typeof track.getCapabilities !== 'function') return null;
const cap = new ImageCapture(track); const cap = imageCap || new ImageCapture(track);
const blob = await cap.takePhoto(); let blob = null;
if (fotoBlitz && blitz !== 'torch') {
// Blitz ausdruecklich setzen (auch „aus"); klappt das nicht, ohne Einstellungen ausloesen
try { blob = await cap.takePhoto({ fillLightMode: blitz }); } catch (_) { blob = null; }
}
if (!blob) blob = await cap.takePhoto();
// Manche Geraete schalten das Dauerlicht beim Ausloesen ab
if (blitz === 'torch') lichtSchalten(true);
return (blob && blob.size > 0) ? blob : null; return (blob && blob.size > 0) ? blob : null;
} catch (_) { } catch (_) {
return null; // z.B. „takePhoto not supported" — Standbild nehmen return null; // z.B. „takePhoto not supported" — Standbild nehmen

View file

@ -74,6 +74,11 @@
const it = await idb.queuePatch(id, (x) => { const it = await idb.queuePatch(id, (x) => {
x.data = buf; x.data = buf;
x.mime = fileBlob.type || x.mime || 'image/jpeg'; x.mime = fileBlob.type || x.mime || 'image/jpeg';
// Verkleinert = JPEG aus dem Canvas: Endung angleichen. Sonst laege JPEG-Inhalt unter
// „.avif"/„.webp" in der Queue, und der Server lehnt die Endung ab.
if (x.mime === 'image/jpeg' && x.filename && !/\.jpe?g$/i.test(x.filename)) {
x.filename = x.filename.replace(/\.[^.\/]*$/, '') + '.jpg';
}
}); });
return !!it; // null: schon hochgeladen und geloescht return !!it; // null: schon hochgeladen und geloescht
} }