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:
parent
0d2f485ecc
commit
91f3f483b5
2 changed files with 154 additions and 4 deletions
153
app.js
153
app.js
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue