rawdecoder.js 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. /*
  2. rawdecoder.js — client side camera RAW decoder for the ArozOS Raw Editor
  3. Responsibilities:
  4. - Detect the container type from the extension / magic bytes.
  5. - For ordinary images (jpg/png/webp/tiff) draw them onto a work canvas.
  6. - For camera RAW (ARW/DNG/NEF/CR2/ORF/RW2 ... — all TIFF/IFD based)
  7. parse the TIFF structure, locate the CFA (Bayer) plane and demosaic it
  8. into an RGB image, applying black/white levels and camera / gray-world
  9. white balance.
  10. - When the raw payload uses a compression we cannot decode, gracefully
  11. fall back to the full size JPEG preview that virtually every RAW file
  12. embeds, so the user always sees their photo.
  13. The decoder always resolves to a common structure consumed by the editor:
  14. {
  15. width, height, // working resolution (long edge capped)
  16. data: Float32Array, // RGBA, scene linear, range ~[0,1]
  17. meta: { camera, iso, shutter, aperture, focal, temp, tint, wb:[r,g,b] },
  18. source: 'raw-demosaic' | 'embedded-preview' | 'image'
  19. }
  20. Everything downstream (WebGL develop pipeline) treats "data" as linear RGBA.
  21. */
  22. const RawDecoder = (function () {
  23. // Longest edge (px) of the working buffer used for interactive editing and
  24. // export. Keeps memory / GPU upload bounded on multi-megapixel sensors.
  25. const MAX_WORK_EDGE = 2560;
  26. const RAW_EXTS = ["arw", "dng", "nef", "cr2", "cr3", "orf", "raf", "rw2", "pef", "srw"];
  27. function extOf(name) {
  28. const i = (name || "").lastIndexOf(".");
  29. return i < 0 ? "" : name.substring(i + 1).toLowerCase();
  30. }
  31. // ---- sRGB <-> linear helpers ------------------------------------------
  32. function srgbToLinear(c) {
  33. return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  34. }
  35. // Convert an 8bit RGBA ImageData (sRGB) into a linear Float32 RGBA buffer.
  36. function imageDataToLinear(imgData) {
  37. const src = imgData.data;
  38. const out = new Float32Array(src.length);
  39. // Small lookup table for the 256 possible 8bit values.
  40. const lut = new Float32Array(256);
  41. for (let i = 0; i < 256; i++) lut[i] = srgbToLinear(i / 255);
  42. for (let i = 0; i < src.length; i += 4) {
  43. out[i] = lut[src[i]];
  44. out[i + 1] = lut[src[i + 1]];
  45. out[i + 2] = lut[src[i + 2]];
  46. out[i + 3] = 1.0;
  47. }
  48. return out;
  49. }
  50. // Draw an ImageBitmap/Image onto a work canvas (capped) and return linear RGBA.
  51. function bitmapToWork(bitmap) {
  52. let w = bitmap.width, h = bitmap.height;
  53. const scale = Math.min(1, MAX_WORK_EDGE / Math.max(w, h));
  54. w = Math.max(1, Math.round(w * scale));
  55. h = Math.max(1, Math.round(h * scale));
  56. const cv = document.createElement("canvas");
  57. cv.width = w; cv.height = h;
  58. const ctx = cv.getContext("2d");
  59. ctx.drawImage(bitmap, 0, 0, w, h);
  60. const img = ctx.getImageData(0, 0, w, h);
  61. return { width: w, height: h, data: imageDataToLinear(img) };
  62. }
  63. function decodeBlobAsImage(blob) {
  64. return new Promise((resolve, reject) => {
  65. if (window.createImageBitmap) {
  66. createImageBitmap(blob).then((bmp) => {
  67. resolve(bitmapToWork(bmp));
  68. }).catch(reject);
  69. } else {
  70. const url = URL.createObjectURL(blob);
  71. const im = new Image();
  72. im.onload = () => { URL.revokeObjectURL(url); resolve(bitmapToWork(im)); };
  73. im.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
  74. im.src = url;
  75. }
  76. });
  77. }
  78. // ======================================================================
  79. // TIFF / IFD parsing
  80. // ======================================================================
  81. const TYPE_SIZE = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8 };
  82. function parseTIFF(buf) {
  83. const dv = new DataView(buf);
  84. if (dv.byteLength < 8) throw new Error("not a tiff");
  85. const b0 = dv.getUint8(0), b1 = dv.getUint8(1);
  86. let little;
  87. if (b0 === 0x49 && b1 === 0x49) little = true; // II
  88. else if (b0 === 0x4D && b1 === 0x4D) little = false; // MM
  89. else throw new Error("not a tiff (byte order)");
  90. const magic = dv.getUint16(2, little);
  91. if (magic !== 42 && magic !== 0x4F52 /*ORF 'RO'*/ && magic !== 0x5352) {
  92. // ORF (Olympus) and some others use a non-42 magic; be lenient.
  93. }
  94. const ifds = [];
  95. const visited = {};
  96. function readValues(dv, type, count, valOffset, entryOffset) {
  97. const size = TYPE_SIZE[type] || 1;
  98. const total = size * count;
  99. let base;
  100. if (total <= 4) base = entryOffset; // inline
  101. else base = valOffset;
  102. if (base + total > dv.byteLength) return null;
  103. const vals = [];
  104. for (let i = 0; i < count; i++) {
  105. const o = base + i * size;
  106. switch (type) {
  107. case 1: case 6: case 7: vals.push(dv.getUint8(o)); break;
  108. case 2: vals.push(dv.getUint8(o)); break; // ascii byte
  109. case 3: case 8: vals.push(dv.getUint16(o, little)); break;
  110. case 4: case 9: vals.push(dv.getUint32(o, little)); break;
  111. case 5: vals.push(dv.getUint32(o, little) / (dv.getUint32(o + 4, little) || 1)); break;
  112. case 10: vals.push(dv.getInt32(o, little) / (dv.getInt32(o + 4, little) || 1)); break;
  113. case 11: vals.push(dv.getFloat32(o, little)); break;
  114. case 12: vals.push(dv.getFloat64(o, little)); break;
  115. default: vals.push(dv.getUint8(o));
  116. }
  117. }
  118. return vals;
  119. }
  120. function readIFD(offset) {
  121. if (!offset || offset <= 0 || offset + 2 > dv.byteLength) return null;
  122. if (visited[offset]) return null;
  123. visited[offset] = true;
  124. const count = dv.getUint16(offset, little);
  125. if (count > 4096) return null; // not a real IFD — bogus offset
  126. const tags = {};
  127. const subOffsets = [];
  128. let p = offset + 2;
  129. for (let i = 0; i < count; i++, p += 12) {
  130. if (p + 12 > dv.byteLength) break;
  131. const tag = dv.getUint16(p, little);
  132. const type = dv.getUint16(p + 2, little);
  133. const cnt = dv.getUint32(p + 4, little);
  134. const valOff = dv.getUint32(p + 8, little);
  135. tags[tag] = { type: type, count: cnt, valueOffset: valOff, entryOffset: p + 8 };
  136. // Follow SubIFD (0x014A) and the ExifIFD (0x8769) pointers. Do NOT
  137. // recurse the MakerNote (0x927C): its bytes are not IFD offsets and
  138. // treating them as such can be pathologically slow on real files.
  139. if (tag === 0x014A || tag === 0x8769) {
  140. const v = readValues(dv, type, cnt, valOff, p + 8);
  141. if (v) v.forEach((o) => subOffsets.push(o));
  142. }
  143. }
  144. const nextOff = (p + 4 <= dv.byteLength) ? dv.getUint32(p, little) : 0;
  145. const ifd = {
  146. tags: tags,
  147. get: function (tag) {
  148. const t = tags[tag];
  149. if (!t) return null;
  150. return readValues(dv, t.type, t.count, t.valueOffset, t.entryOffset);
  151. },
  152. raw: tags
  153. };
  154. ifds.push(ifd);
  155. subOffsets.forEach((so) => readIFD(so));
  156. return nextOff;
  157. }
  158. let ifdOff = dv.getUint32(4, little);
  159. let guard = 0;
  160. while (ifdOff && guard++ < 64) {
  161. ifdOff = readIFD(ifdOff);
  162. }
  163. return { dv: dv, little: little, ifds: ifds };
  164. }
  165. // Common TIFF/EXIF/DNG tag ids we care about.
  166. const T = {
  167. ImageWidth: 0x0100, ImageLength: 0x0101, BitsPerSample: 0x0102,
  168. Compression: 0x0103, Photometric: 0x0106, StripOffsets: 0x0111,
  169. RowsPerStrip: 0x0116, StripByteCounts: 0x0117, TileWidth: 0x0142,
  170. TileLength: 0x0143, TileOffsets: 0x0144, TileByteCounts: 0x0145,
  171. JPEGOffset: 0x0201, JPEGLength: 0x0202, Make: 0x010F, Model: 0x0110,
  172. CFAPattern: 0x828E, CFAPatternExif: 0xA302, SubfileType: 0x00FE,
  173. // EXIF
  174. ExposureTime: 0x829A, FNumber: 0x829D, ISO: 0x8827, FocalLength: 0x920A,
  175. // DNG
  176. BlackLevel: 0xC61A, WhiteLevel: 0xC61D, AsShotNeutral: 0xC628,
  177. DNGVersion: 0xC612, CFALayout: 0xC61E, LinearizationTable: 0xC618
  178. };
  179. function asciiOf(vals) {
  180. if (!vals) return "";
  181. let s = "";
  182. for (let i = 0; i < vals.length; i++) {
  183. if (vals[i] === 0) break;
  184. s += String.fromCharCode(vals[i]);
  185. }
  186. return s.trim();
  187. }
  188. // ======================================================================
  189. // Embedded JPEG preview extraction (robust fallback)
  190. // ======================================================================
  191. // True only for a real, decodable JPEG (SOI at start, EOI at end). This
  192. // rejects lossless-JPEG-compressed CFA raw streams, which also start with
  193. // 0xFFD8 but are NOT displayable images.
  194. function looksLikeJpeg(u8, off, len) {
  195. return off >= 0 && len > 1000 && off + len <= u8.length &&
  196. u8[off] === 0xFF && u8[off + 1] === 0xD8 &&
  197. u8[off + len - 2] === 0xFF && u8[off + len - 1] === 0xD9;
  198. }
  199. // Locate the largest embedded preview JPEG, using IFD pointers first then a
  200. // brute force SOI/EOI scan. CFA / raw IFDs (Photometric 32803) are ignored —
  201. // their compressed data starts with 0xFFD8 but is not a viewable image.
  202. // Returns { off, len } into the source bytes or null.
  203. function findEmbeddedJpegRange(u8, tiff) {
  204. let best = null;
  205. if (tiff) {
  206. tiff.ifds.forEach((ifd) => {
  207. const photo = ifd.get(T.Photometric);
  208. if (photo && photo[0] === 32803) return; // CFA raw — never a preview
  209. const off = ifd.get(T.JPEGOffset);
  210. const len = ifd.get(T.JPEGLength);
  211. if (off && len && looksLikeJpeg(u8, off[0], len[0])) {
  212. if (!best || len[0] > best.len) best = { off: off[0], len: len[0] };
  213. }
  214. // Some previews are stored as a full strip with Compression 6/7.
  215. const comp = ifd.get(T.Compression);
  216. if (comp && (comp[0] === 6 || comp[0] === 7 || comp[0] === 99)) {
  217. const so = ifd.get(T.StripOffsets);
  218. const sc = ifd.get(T.StripByteCounts);
  219. if (so && sc && so.length === 1 && looksLikeJpeg(u8, so[0], sc[0])) {
  220. if (!best || sc[0] > best.len) best = { off: so[0], len: sc[0] };
  221. }
  222. }
  223. });
  224. }
  225. if (best) return best;
  226. // Brute force: find the largest FFD8..FFD9 span.
  227. let bestStart = -1, bestEnd = -1;
  228. for (let i = 0; i + 1 < u8.length; i++) {
  229. if (u8[i] === 0xFF && u8[i + 1] === 0xD8 && u8[i + 2] === 0xFF) {
  230. for (let j = i + 2; j + 1 < u8.length; j++) {
  231. if (u8[j] === 0xFF && u8[j + 1] === 0xD9) {
  232. if (j - i > bestEnd - bestStart) { bestStart = i; bestEnd = j + 1; }
  233. i = j + 1;
  234. break;
  235. }
  236. }
  237. }
  238. }
  239. if (bestStart >= 0 && bestEnd - bestStart > 2000) {
  240. return { off: bestStart, len: bestEnd - bestStart + 1 };
  241. }
  242. return null;
  243. }
  244. function extractEmbeddedJpeg(buf, tiff) {
  245. const u8 = new Uint8Array(buf);
  246. const r = findEmbeddedJpegRange(u8, tiff);
  247. return r ? new Blob([u8.subarray(r.off, r.off + r.len)], { type: "image/jpeg" }) : null;
  248. }
  249. // ======================================================================
  250. // EXIF from a JPEG APP1 segment (covers plain JPEGs, embedded previews,
  251. // and RAWs whose main TIFF structure we could not parse).
  252. // ======================================================================
  253. // jbytes: a Uint8Array starting at a JPEG SOI (0xFFD8). Returns meta or null.
  254. function parseJpegExif(jbytes) {
  255. if (!jbytes || jbytes.length < 4 || jbytes[0] !== 0xFF || jbytes[1] !== 0xD8) return null;
  256. let i = 2;
  257. while (i + 4 < jbytes.length) {
  258. if (jbytes[i] !== 0xFF) { i++; continue; }
  259. const marker = jbytes[i + 1];
  260. if (marker === 0xD9 || marker === 0xDA) break; // EOI / start of scan
  261. const len = (jbytes[i + 2] << 8) | jbytes[i + 3];
  262. if (len < 2) break;
  263. if (marker === 0xE1) {
  264. const o = i + 4;
  265. // "Exif\0\0"
  266. if (jbytes[o] === 0x45 && jbytes[o + 1] === 0x78 && jbytes[o + 2] === 0x69 &&
  267. jbytes[o + 3] === 0x66 && jbytes[o + 4] === 0 && jbytes[o + 5] === 0) {
  268. const tiffStart = o + 6;
  269. const tiffLen = (len - 2) - 6;
  270. if (tiffLen > 8 && tiffStart + tiffLen <= jbytes.length) {
  271. try {
  272. const sub = jbytes.slice(tiffStart, tiffStart + tiffLen).buffer;
  273. return readMeta(parseTIFF(sub));
  274. } catch (e) { return null; }
  275. }
  276. }
  277. }
  278. i += 2 + len;
  279. }
  280. return null;
  281. }
  282. function metaHasExif(m) {
  283. return m && (m.iso || m.aperture || m.shutter || m.focal || m.camera);
  284. }
  285. function mergeMeta(primary, secondary) {
  286. if (!secondary) return primary;
  287. if (!primary) return secondary;
  288. const out = Object.assign({}, primary);
  289. ["camera", "iso", "shutter", "aperture", "focal"].forEach((k) => {
  290. if ((out[k] === "" || out[k] === 0 || out[k] == null) && secondary[k]) out[k] = secondary[k];
  291. });
  292. if (!out.wb && secondary.wb) out.wb = secondary.wb;
  293. if ((!out.temp || out.temp === 5500) && secondary.temp) out.temp = secondary.temp;
  294. return out;
  295. }
  296. // Collect every embedded JPEG (IFD-pointed + brute-force SOI/EOI scan).
  297. // Sony ARW keeps EXIF in the small thumbnail, not the large preview, so we
  298. // must be able to inspect all of them — not just the biggest.
  299. function collectJpegRanges(u8, tiff) {
  300. const ranges = [];
  301. const seen = {};
  302. const add = (off, len) => {
  303. if (off >= 0 && len > 100 && off + len <= u8.length &&
  304. u8[off] === 0xFF && u8[off + 1] === 0xD8 && !seen[off]) {
  305. seen[off] = true; ranges.push({ off: off, len: len });
  306. }
  307. };
  308. if (tiff) {
  309. tiff.ifds.forEach((ifd) => {
  310. const photo = ifd.get(T.Photometric);
  311. if (photo && photo[0] === 32803) return; // skip CFA raw stream
  312. const off = ifd.get(T.JPEGOffset), len = ifd.get(T.JPEGLength);
  313. if (off && len) add(off[0], len[0]);
  314. const so = ifd.get(T.StripOffsets), sc = ifd.get(T.StripByteCounts);
  315. if (so && sc && so.length === 1) add(so[0], sc[0]);
  316. });
  317. }
  318. // Brute-force SOI/EOI scan, capped so a huge false-positive (e.g. raw
  319. // data containing 0xFFD8) can't produce a multi-megabyte junk range.
  320. for (let i = 0; i + 2 < u8.length; i++) {
  321. if (u8[i] === 0xFF && u8[i + 1] === 0xD8 && u8[i + 2] === 0xFF) {
  322. for (let j = i + 2; j + 1 < u8.length && j - i < 4000000; j++) {
  323. if (u8[j] === 0xFF && u8[j + 1] === 0xD9) { add(i, j - i + 1); i = j + 1; break; }
  324. }
  325. }
  326. }
  327. return ranges;
  328. }
  329. // Fill gaps in "meta" from EXIF found in any embedded / whole-file JPEG.
  330. function enrichMetaFromJpeg(buf, tiff, meta) {
  331. if (metaHasExif(meta) && meta.iso && meta.aperture && meta.shutter) return meta;
  332. try {
  333. const u8 = new Uint8Array(buf);
  334. if (u8[0] === 0xFF && u8[1] === 0xD8) {
  335. const em0 = parseJpegExif(u8);
  336. if (em0) meta = mergeMeta(meta, em0);
  337. }
  338. const ranges = collectJpegRanges(u8, tiff);
  339. for (let k = 0; k < ranges.length; k++) {
  340. if (meta.iso && meta.aperture && meta.shutter) break;
  341. const em = parseJpegExif(u8.subarray(ranges[k].off, ranges[k].off + ranges[k].len));
  342. if (em) meta = mergeMeta(meta, em);
  343. }
  344. return meta;
  345. } catch (e) { return meta; }
  346. }
  347. // ======================================================================
  348. // CFA (Bayer) extraction + demosaic
  349. // ======================================================================
  350. // Read raw CFA samples into a Float32 single-channel plane (values kept in
  351. // sensor code range). Supports uncompressed 16/14/12-bit (packed or not)
  352. // and Sony ARW2 lossy compression. Returns null when unsupported.
  353. function readCFAPlane(buf, tiff, ifd) {
  354. const dv = tiff.dv, little = tiff.little;
  355. const w = (ifd.get(T.ImageWidth) || [0])[0];
  356. const h = (ifd.get(T.ImageLength) || [0])[0];
  357. const bps = (ifd.get(T.BitsPerSample) || [16])[0];
  358. const comp = (ifd.get(T.Compression) || [1])[0];
  359. if (!w || !h || w * h > 80e6) return null;
  360. const plane = new Float32Array(w * h);
  361. const strips = ifd.get(T.StripOffsets);
  362. const counts = ifd.get(T.StripByteCounts);
  363. const rowsPerStrip = (ifd.get(T.RowsPerStrip) || [h])[0];
  364. if (comp === 1) {
  365. // Uncompressed. Concatenate strips then unpack bit-by-bit.
  366. if (!strips) return null;
  367. let dstPix = 0;
  368. for (let s = 0; s < strips.length; s++) {
  369. const off = strips[s];
  370. const nRows = Math.min(rowsPerStrip, h - s * rowsPerStrip);
  371. const pixInStrip = nRows * w;
  372. if (bps === 16) {
  373. for (let i = 0; i < pixInStrip; i++) {
  374. const o = off + i * 2;
  375. if (o + 1 >= dv.byteLength) break;
  376. plane[dstPix++] = dv.getUint16(o, little);
  377. }
  378. } else {
  379. // Packed bitstream (12 or 14 bit), MSB first per TIFF spec.
  380. let bitPos = off * 8;
  381. for (let i = 0; i < pixInStrip; i++) {
  382. let v = 0;
  383. for (let b = 0; b < bps; b++) {
  384. const bytePos = bitPos >> 3;
  385. if (bytePos >= dv.byteLength) { v = 0; break; }
  386. const bit = 7 - (bitPos & 7);
  387. v = (v << 1) | ((dv.getUint8(bytePos) >> bit) & 1);
  388. bitPos++;
  389. }
  390. plane[dstPix++] = v;
  391. }
  392. }
  393. }
  394. return { plane: plane, width: w, height: h, maxVal: (1 << bps) - 1 };
  395. }
  396. if (comp === 32767) {
  397. // Sony ARW2 lossy compression: 16 pixel blocks, each 128 bits.
  398. if (!strips || !counts) return null;
  399. if (decodeSonyARW2(dv, strips, counts, w, h, plane)) {
  400. return { plane: plane, width: w, height: h, maxVal: 16383 };
  401. }
  402. return null;
  403. }
  404. return null; // unsupported compression (lossless JPEG etc.)
  405. }
  406. // Sony ARW2 block decoder. Each row is stored in 16 pixel groups; a group
  407. // is 16 bytes = 2x max/min (11bit each) + 4bit shift + 14x 7bit deltas.
  408. // Reference: dcraw sony_arw2_load_raw. Values are interleaved per Bayer
  409. // colour (even/odd columns) but we lay them out linearly which is fine for
  410. // a subsequent generic demosaic.
  411. function decodeSonyARW2(dv, strips, counts, w, h, plane) {
  412. try {
  413. // Build a per-row byte offset table from strips.
  414. // ARW2 typically uses one strip; bytes-per-row = stripBytes / rows.
  415. const totalBytes = counts.reduce((a, b) => a + b, 0);
  416. const bytesPerRow = Math.floor(totalBytes / h);
  417. if (bytesPerRow < w) return false;
  418. function bits(bytePos, bitOff, n) {
  419. let v = 0;
  420. for (let i = 0; i < n; i++) {
  421. const bp = bytePos + ((bitOff + i) >> 3);
  422. if (bp >= dv.byteLength) return v;
  423. const b = 7 - ((bitOff + i) & 7);
  424. v = (v << 1) | ((dv.getUint8(bp) >> b) & 1);
  425. }
  426. return v;
  427. }
  428. let rowBase = strips[0];
  429. for (let row = 0; row < h; row++) {
  430. let colByte = rowBase;
  431. for (let col = 0; col < w; col += 16) {
  432. // Two interleaved colours -> two passes of 8 pixels.
  433. for (let phase = 0; phase < 2; phase++) {
  434. let bitOff = 0;
  435. const max = bits(colByte, bitOff, 11); bitOff += 11;
  436. const min = bits(colByte, bitOff, 11); bitOff += 11;
  437. const imax = bits(colByte, bitOff, 4); bitOff += 4;
  438. const imin = bits(colByte, bitOff, 4); bitOff += 4;
  439. let sh = 0;
  440. for (let s = max; s < 0x800; s <<= 1) sh++;
  441. for (let i = 0; i < 8; i++) {
  442. let p;
  443. if (i === imax) p = max;
  444. else if (i === imin) p = min;
  445. else {
  446. const d = bits(colByte, bitOff, 7); bitOff += 7;
  447. p = (d << sh) + min;
  448. if (p > 0x7ff) p = 0x7ff;
  449. }
  450. const c = col + i * 2 + phase;
  451. if (c < w) plane[row * w + c] = p << 1;
  452. }
  453. colByte += 16;
  454. }
  455. }
  456. rowBase += bytesPerRow;
  457. }
  458. return true;
  459. } catch (e) {
  460. return false;
  461. }
  462. }
  463. // Bilinear demosaic of a single CFA plane into linear RGBA float, applying
  464. // black/white level normalisation and camera / gray-world white balance.
  465. function demosaic(cfa, pattern, black, white, wbMul) {
  466. const w = cfa.width, h = cfa.height, p = cfa.plane;
  467. const out = new Float32Array(w * h * 4);
  468. const range = Math.max(1, (white - black));
  469. // pattern: 2x2 colour ids, 0=R 1=G 2=B for (row%2,col%2).
  470. function colorAt(y, x) { return pattern[(y & 1) * 2 + (x & 1)]; }
  471. function samp(y, x) {
  472. if (x < 0) x = 1; if (x >= w) x = w - 2;
  473. if (y < 0) y = 1; if (y >= h) y = h - 2;
  474. return p[y * w + x];
  475. }
  476. for (let y = 0; y < h; y++) {
  477. for (let x = 0; x < w; x++) {
  478. const c = colorAt(y, x);
  479. let r, g, b;
  480. const v = samp(y, x);
  481. if (c === 0) { // red site
  482. r = v;
  483. g = (samp(y, x - 1) + samp(y, x + 1) + samp(y - 1, x) + samp(y + 1, x)) * 0.25;
  484. b = (samp(y - 1, x - 1) + samp(y - 1, x + 1) + samp(y + 1, x - 1) + samp(y + 1, x + 1)) * 0.25;
  485. } else if (c === 2) { // blue site
  486. b = v;
  487. g = (samp(y, x - 1) + samp(y, x + 1) + samp(y - 1, x) + samp(y + 1, x)) * 0.25;
  488. r = (samp(y - 1, x - 1) + samp(y - 1, x + 1) + samp(y + 1, x - 1) + samp(y + 1, x + 1)) * 0.25;
  489. } else { // green site
  490. g = v;
  491. // Neighbours: horizontal & vertical carry the two other colours.
  492. const h1 = samp(y, x - 1), h2 = samp(y, x + 1);
  493. const v1 = samp(y - 1, x), v2 = samp(y + 1, x);
  494. if (colorAt(y, x - 1) === 0) { r = (h1 + h2) * 0.5; b = (v1 + v2) * 0.5; }
  495. else { b = (h1 + h2) * 0.5; r = (v1 + v2) * 0.5; }
  496. }
  497. const o = (y * w + x) * 4;
  498. out[o] = Math.max(0, (r - black) / range) * wbMul[0];
  499. out[o + 1] = Math.max(0, (g - black) / range) * wbMul[1];
  500. out[o + 2] = Math.max(0, (b - black) / range) * wbMul[2];
  501. out[o + 3] = 1.0;
  502. }
  503. }
  504. return { data: out, width: w, height: h };
  505. }
  506. // Downscale a full-res linear RGBA buffer to the working edge cap (box filter).
  507. function downscaleLinear(src, w, h) {
  508. const scale = Math.min(1, MAX_WORK_EDGE / Math.max(w, h));
  509. if (scale >= 1) return { data: src, width: w, height: h };
  510. const nw = Math.max(1, Math.round(w * scale));
  511. const nh = Math.max(1, Math.round(h * scale));
  512. const out = new Float32Array(nw * nh * 4);
  513. const sx = w / nw, sy = h / nh;
  514. for (let y = 0; y < nh; y++) {
  515. const y0 = Math.floor(y * sy), y1 = Math.min(h, Math.floor((y + 1) * sy));
  516. for (let x = 0; x < nw; x++) {
  517. const x0 = Math.floor(x * sx), x1 = Math.min(w, Math.floor((x + 1) * sx));
  518. let r = 0, g = 0, b = 0, n = 0;
  519. for (let yy = y0; yy < y1; yy++) {
  520. for (let xx = x0; xx < x1; xx++) {
  521. const o = (yy * w + xx) * 4;
  522. r += src[o]; g += src[o + 1]; b += src[o + 2]; n++;
  523. }
  524. }
  525. const o = (y * nw + x) * 4;
  526. if (n === 0) n = 1;
  527. out[o] = r / n; out[o + 1] = g / n; out[o + 2] = b / n; out[o + 3] = 1;
  528. }
  529. }
  530. return { data: out, width: nw, height: nh };
  531. }
  532. // Estimate a gray-world white balance from the demosaiced plane, used when
  533. // the file carries no AsShotNeutral (typical for Sony ARW without makernote).
  534. function grayWorldMul(cfa, pattern, black) {
  535. const w = cfa.width, h = cfa.height, p = cfa.plane;
  536. let sr = 0, sg = 0, sb = 0, nr = 0, ng = 0, nb = 0;
  537. const step = Math.max(1, Math.floor(Math.min(w, h) / 300));
  538. for (let y = 0; y < h; y += step) {
  539. for (let x = 0; x < w; x += step) {
  540. const v = Math.max(0, p[y * w + x] - black);
  541. const c = pattern[(y & 1) * 2 + (x & 1)];
  542. if (c === 0) { sr += v; nr++; }
  543. else if (c === 2) { sb += v; nb++; }
  544. else { sg += v; ng++; }
  545. }
  546. }
  547. const ar = sr / Math.max(1, nr), ag = sg / Math.max(1, ng), ab = sb / Math.max(1, nb);
  548. const g = ag || 1;
  549. let mr = g / (ar || g), mb = g / (ab || g);
  550. // clamp to sane range
  551. mr = Math.min(4, Math.max(0.25, mr));
  552. mb = Math.min(4, Math.max(0.25, mb));
  553. return [mr, 1.0, mb];
  554. }
  555. // Map a DNG CFAPattern (or default) into our 2x2 colour id layout.
  556. function resolvePattern(ifd) {
  557. const raw = ifd.get(T.CFAPattern) || ifd.get(T.CFAPatternExif);
  558. // CFAPattern values: 0=R 1=G 2=B (Exif) preceded by a 2x2 dim header in
  559. // some encodings; be defensive and fall back to RGGB.
  560. if (raw && raw.length >= 4) {
  561. const last4 = raw.slice(raw.length - 4);
  562. const ok = last4.every((v) => v >= 0 && v <= 2);
  563. if (ok) return last4;
  564. }
  565. return [0, 1, 1, 2]; // RGGB
  566. }
  567. function readMeta(tiff) {
  568. const meta = { camera: "", iso: 0, shutter: 0, aperture: 0, focal: 0, temp: 5500, tint: 0, wb: null };
  569. tiff.ifds.forEach((ifd) => {
  570. const make = asciiOf(ifd.get(T.Make));
  571. const model = asciiOf(ifd.get(T.Model));
  572. if (model && !meta.camera) meta.camera = (make ? make + " " : "") + model;
  573. const iso = ifd.get(T.ISO); if (iso && !meta.iso) meta.iso = iso[0];
  574. const et = ifd.get(T.ExposureTime); if (et && !meta.shutter) meta.shutter = et[0];
  575. const fn = ifd.get(T.FNumber); if (fn && !meta.aperture) meta.aperture = fn[0];
  576. const fl = ifd.get(T.FocalLength); if (fl && !meta.focal) meta.focal = fl[0];
  577. const asn = ifd.get(T.AsShotNeutral);
  578. if (asn && asn.length >= 3 && !meta.wb) {
  579. // AsShotNeutral is the neutral in camera-native space; multiplier is 1/n.
  580. meta.wb = [1 / (asn[0] || 1), 1 / (asn[1] || 1), 1 / (asn[2] || 1)];
  581. const g = meta.wb[1] || 1;
  582. meta.wb = [meta.wb[0] / g, 1, meta.wb[2] / g];
  583. }
  584. });
  585. return meta;
  586. }
  587. // Attempt a full CFA demosaic. Returns work-sized linear RGBA or null.
  588. function tryDemosaic(buf, tiff, meta) {
  589. // Find the CFA IFD: Photometric 32803, or the largest raw-looking plane.
  590. let cfaIfd = null, bestPix = 0;
  591. tiff.ifds.forEach((ifd) => {
  592. const photo = ifd.get(T.Photometric);
  593. const w = (ifd.get(T.ImageWidth) || [0])[0];
  594. const h = (ifd.get(T.ImageLength) || [0])[0];
  595. const comp = (ifd.get(T.Compression) || [0])[0];
  596. const isCFA = photo && photo[0] === 32803;
  597. if ((isCFA || comp === 32767) && w * h > bestPix) { cfaIfd = ifd; bestPix = w * h; }
  598. });
  599. if (!cfaIfd) return null;
  600. const cfa = readCFAPlane(buf, tiff, cfaIfd);
  601. if (!cfa) return null;
  602. const pattern = resolvePattern(cfaIfd);
  603. let black = (cfaIfd.get(T.BlackLevel) || [0])[0] || 0;
  604. let white = (cfaIfd.get(T.WhiteLevel) || [cfa.maxVal])[0] || cfa.maxVal;
  605. if (white <= black) white = cfa.maxVal;
  606. const wb = meta.wb || grayWorldMul(cfa, pattern, black);
  607. const dem = demosaic(cfa, pattern, black, white, wb);
  608. // A mild highlight normalise so mid-tones land in a sensible range.
  609. return downscaleLinear(dem.data, dem.width, dem.height);
  610. }
  611. // ======================================================================
  612. // Public entry point
  613. // ======================================================================
  614. function decode(buf, filename) {
  615. return new Promise((resolve, reject) => {
  616. const ext = extOf(filename);
  617. const blob = new Blob([buf]);
  618. // Plain images: hand straight to the browser, but still read EXIF
  619. // from the JPEG APP1 segment so shooting info is shown.
  620. if (["jpg", "jpeg", "png", "webp", "gif", "bmp"].indexOf(ext) >= 0) {
  621. let imgMeta = emptyMeta();
  622. try { imgMeta = enrichMetaFromJpeg(buf, null, imgMeta); } catch (e) { imgMeta = emptyMeta(); }
  623. decodeBlobAsImage(blob).then((work) => {
  624. resolve({ width: work.width, height: work.height, data: work.data, meta: imgMeta, source: "image" });
  625. }).catch(reject);
  626. return;
  627. }
  628. let tiff = null;
  629. try { tiff = parseTIFF(buf); } catch (e) { tiff = null; }
  630. let meta = tiff ? readMeta(tiff) : emptyMeta();
  631. // Fill any missing EXIF from the embedded JPEG preview — this rescues
  632. // shooting info when the RAW's TIFF structure could not be parsed.
  633. try { meta = enrichMetaFromJpeg(buf, tiff, meta); } catch (e) { /* keep meta */ }
  634. // For RAW containers, try to demosaic; otherwise fall back to preview.
  635. const isRaw = RAW_EXTS.indexOf(ext) >= 0 || (tiff && ext !== "tif" && ext !== "tiff");
  636. function finishWithPreview() {
  637. const jpeg = tiff ? extractEmbeddedJpeg(buf, tiff) : null;
  638. if (jpeg) {
  639. decodeBlobAsImage(jpeg).then((work) => {
  640. resolve({ width: work.width, height: work.height, data: work.data, meta: meta, source: "embedded-preview" });
  641. }).catch(() => tryTiffAsImage());
  642. } else {
  643. tryTiffAsImage();
  644. }
  645. }
  646. function tryTiffAsImage() {
  647. // Last resort: maybe the browser can render it (baseline TIFF/DNG w/ preview).
  648. decodeBlobAsImage(blob).then((work) => {
  649. resolve({ width: work.width, height: work.height, data: work.data, meta: meta, source: "image" });
  650. }).catch(() => reject(new Error("Unable to decode this file. The RAW format or its compression is not supported and no embedded preview was found.")));
  651. }
  652. if (isRaw && tiff) {
  653. let dem = null;
  654. try { dem = tryDemosaic(buf, tiff, meta); } catch (e) { dem = null; }
  655. if (dem) {
  656. resolve({ width: dem.width, height: dem.height, data: dem.data, meta: meta, source: "raw-demosaic" });
  657. return;
  658. }
  659. finishWithPreview();
  660. return;
  661. }
  662. // tif/tiff or anything else: try as image, then preview.
  663. decodeBlobAsImage(blob).then((work) => {
  664. resolve({ width: work.width, height: work.height, data: work.data, meta: meta, source: "image" });
  665. }).catch(finishWithPreview);
  666. });
  667. }
  668. function emptyMeta() {
  669. return { camera: "", iso: 0, shutter: 0, aperture: 0, focal: 0, temp: 5500, tint: 0, wb: null };
  670. }
  671. return { decode: decode, MAX_WORK_EDGE: MAX_WORK_EDGE };
  672. })();