← back to Dw Photo Capture
public/js/native-photo.js
67 lines
/*
* native-photo.js — full-resolution capture via the device's OWN camera app.
*
* getUserMedia video frames are capped by the stream (iPhone Safari often delivers ~1920 wide no
* matter what we ask for). <input type=file accept=image/* capture=environment> instead opens the
* native Camera app and hands back the real sensor photo (12MP / 4032x3024 on a current iPhone).
*
* NativePhoto.pick() -> Promise<{ full, small, w, h } | null>
* full — JPEG 0.92, long edge <= FULL_EDGE (4096 keeps a whole 12MP shot; a 48MP shot is scaled
* so the base64 body stays well under the server's 25MB cap). Use for /api/create-item.
* small — JPEG 0.85, long edge <= SMALL_EDGE. Use for /api/extract: Gemini bills big images by
* tile, and label OCR does not need 12MP, so this keeps the read fast and cheap.
* null — nothing picked, or the photo could not be decoded (callers toast + stay put).
* The re-encode through <img> + canvas applies EXIF orientation and turns HEIC into JPEG.
* The promise may never settle if the user cancels on an older WebKit (no 'cancel' event), so
* callers must not lock their UI while waiting.
*/
(function () {
var FULL_EDGE = 4096, SMALL_EDGE = 1600;
var input = null, pending = null;
function encode(img, maxEdge, q) {
var sw = img.naturalWidth, sh = img.naturalHeight;
var k = Math.min(1, maxEdge / Math.max(sw, sh));
var c = document.createElement('canvas');
c.width = Math.round(sw * k); c.height = Math.round(sh * k);
c.getContext('2d').drawImage(img, 0, 0, c.width, c.height);
return { url: c.toDataURL('image/jpeg', q), w: c.width, h: c.height };
}
function settle(v) { var p = pending; pending = null; if (p) p(v); }
function ensureInput() {
if (input) return input;
input = document.createElement('input');
input.type = 'file'; input.accept = 'image/*';
input.setAttribute('capture', 'environment');
input.style.display = 'none';
input.addEventListener('cancel', function () { settle(null); });
input.addEventListener('change', function () {
var f = input.files && input.files[0];
input.value = '';
if (!f) return settle(null);
var src = URL.createObjectURL(f), img = new Image();
img.onload = function () {
try {
var full = encode(img, FULL_EDGE, 0.92), small = encode(img, SMALL_EDGE, 0.85);
settle({ full: full.url, small: small.url, w: full.w, h: full.h });
} catch (e) { console.error('[native-photo] encode failed', e); settle(null); }
URL.revokeObjectURL(src);
};
img.onerror = function () { URL.revokeObjectURL(src); settle(null); };
img.src = src;
});
document.body.appendChild(input);
return input;
}
window.NativePhoto = {
FULL_EDGE: FULL_EDGE,
pick: function () {
settle(null); // a newer pick supersedes one whose cancel never fired
return new Promise(function (res) { pending = res; ensureInput().click(); });
}
};
})();