import { readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import ts from 'typescript'; const transpile = (path) => ts.transpileModule(readFileSync(new URL(path, import.meta.url), 'utf8'), { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, }).outputText; const LIBRAW_NODE_SRC = ` import { Worker } from 'node:worker_threads'; const SHIM = \` import { parentPort, workerData } from 'node:worker_threads'; import { readFile } from 'node:fs/promises'; globalThis.self = globalThis; globalThis.postMessage = (m, t) => parentPort.postMessage(m, t); Object.defineProperty(globalThis, 'onmessage', { configurable: true, set(fn) { parentPort.on('message', (data) => fn({ data })); }, }); const base = new URL('.', workerData.url); globalThis.fetch = async (u) => new Response(await readFile(new URL(u, base)), { headers: { 'content-type': 'application/wasm' } }); await import(workerData.url); \`; export default class LibRaw { constructor() { const url = new URL('worker.js', ${JSON.stringify(import.meta.resolve('libraw-wasm/dist/index.js'))}).href; this.worker = new Worker(SHIM, { eval: true, workerData: { url } }); this.pending = new Map(); this.next = 0; this.worker.on('message', ({ id, out, error }) => { const p = this.pending.get(id); if (!p) return; this.pending.delete(id); if (error) p.reject(new Error(error)); else p.resolve(out); }); } run(fn, ...args) { return new Promise((resolve, reject) => { const id = this.next++; this.pending.set(id, { resolve, reject }); const transfer = args.filter((a) => ArrayBuffer.isView(a)).map((a) => a.buffer); this.worker.postMessage({ id, fn, args }, transfer); }); } open(bytes, settings) { const copy = Uint8Array.from(bytes); return this.run('open', copy, settings); } metadata(full) { return this.run('metadata', !!full); } imageData() { return this.run('imageData'); } thumbnailData() { return this.run('thumbnailData'); } dispose() { return this.worker.terminate(); } } `; const dir = join(tmpdir(), 'test-all-raws-acc'); import { mkdtempSync } from 'node:fs'; const testDir = mkdtempSync(dir); const ckEntry = import.meta.resolve('canvaskit-wasm/full'); writeFileSync( join(testDir, 'skiaShim.mjs'), transpile('../src/engine/skiaShim.ts').replace( /^import CanvasKitInit from ['"]canvaskit-wasm\/full['"];$/m, `import CanvasKitInit from ${JSON.stringify(ckEntry)};`, ), ); writeFileSync(join(testDir, 'sensorWhite.mjs'), transpile('../src/engine/sensorWhite.ts')); writeFileSync(join(testDir, 'halfFloat.mjs'), transpile('../src/engine/halfFloat.ts')); writeFileSync(join(testDir, 'previewMatch.mjs'), transpile('../src/engine/previewMatch.ts')); writeFileSync( join(testDir, 'tiffDecode.mjs'), transpile('../src/engine/tiffDecode.ts').replace(/['"]\.\/skiaShim['"]/, '"./skiaShim.mjs"') ); // Transpile rawDevelop.ts with fix let rawDevTs = readFileSync(new URL('../src/engine/rawDevelop.ts', import.meta.url), 'utf8'); // Fix WB multiplier order in rawDevelop.ts code before transpiling rawDevTs = rawDevTs.replace( ` if (cd?.cam_mul && Array.isArray(cd.cam_mul) && cd.cam_mul.some((v: number) => v > 0 && v !== 1)) { return cd.cam_mul; } // 2. Try pre-calculated WB multipliers from LibRaw (pre_mul) if (cd?.pre_mul && Array.isArray(cd.pre_mul) && cd.pre_mul.some((v: number) => v > 0 && v !== 1)) { return cd.pre_mul; }`, ` if (cd?.pre_mul && Array.isArray(cd.pre_mul) && cd.pre_mul.some((v: number) => v > 0 && v !== 1)) { return cd.pre_mul; } // 2. Try camera WB multipliers from LibRaw (cam_mul) if (cd?.cam_mul && Array.isArray(cd.cam_mul) && cd.cam_mul.some((v: number) => v > 0 && v !== 1)) { return cd.cam_mul; }` ); const rawDevelopCode = ts.transpileModule(rawDevTs, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, }).outputText .replace(/^import LibRaw from ['"]libraw-wasm['"];$/m, 'import LibRaw from "./librawNode.mjs";') .replace(/['"]\.\/sensorWhite['"]/, '"./sensorWhite.mjs"') .replace(/['"]\.\/halfFloat['"]/, '"./halfFloat.mjs"') .replace(/['"]\.\/previewMatch['"]/, '"./previewMatch.mjs"') .replace(/['"]\.\/skiaShim['"]/, '"./skiaShim.mjs"') .replace(/['"]\.\/tiffDecode['"]/, '"./tiffDecode.mjs"'); writeFileSync(join(testDir, 'rawDevelop.mjs'), rawDevelopCode); writeFileSync(join(testDir, 'librawNode.mjs'), LIBRAW_NODE_SRC); const { developRaw, extractEmbeddedJpeg } = await import(pathToFileURL(join(testDir, 'rawDevelop.mjs')).href); const Skia = await (await import(pathToFileURL(join(testDir, 'skiaShim.mjs')).href)).initSkia(); const getAvgRgb = (img) => { const w = 64, h = 64; const surface = Skia.Surface.MakeOffscreen(w, h); const canvas = surface.getCanvas(); canvas.drawImageRectCubic(img, Skia.XYWHRect(0, 0, img.width(), img.height()), Skia.XYWHRect(0, 0, w, h), 1/3, 1/3); surface.flush(); const px = canvas.readPixels(0, 0, { width: w, height: h, colorType: Skia.ColorType.RGBA_8888, alphaType: Skia.AlphaType.Unpremul, colorSpace: Skia.ColorSpace.SRGB }); surface.dispose(); let r = 0, g = 0, b = 0; for (let i = 0; i < px.length; i += 4) { r += px[i]; g += px[i+1]; b += px[i+2]; } const count = px.length / 4; return [ (r/count).toFixed(2), (g/count).toFixed(2), (b/count).toFixed(2) ]; }; const sampleDir = '../sample'; const files = readdirSync(sampleDir).filter(f => /\.(raf|nef|dng|rw2|orf|arw|cr2|cr3)$/i.test(f)); for (const f of files) { const filePath = join(sampleDir, f); const bytes = new Uint8Array(readFileSync(filePath)); console.log(`\n=== Testing ${f} ===`); try { const devJpeg = await developRaw(bytes, f); const embJpeg = extractEmbeddedJpeg(bytes); const devImg = Skia.Image.MakeImageFromEncoded(devJpeg); const embImg = embJpeg ? Skia.Image.MakeImageFromEncoded(embJpeg) : null; const devRgb = getAvgRgb(devImg); console.log(`Dev dim: ${devImg.width()}x${devImg.height()} RGB: [ ${devRgb.join(', ')} ]`); if (embImg) { const embRgb = getAvgRgb(embImg); console.log(`Emb dim: ${embImg.width()}x${embImg.height()} RGB: [ ${embRgb.join(', ')} ]`); const rDiff = Math.abs(parseFloat(devRgb[0]) - parseFloat(embRgb[0])); const gDiff = Math.abs(parseFloat(devRgb[1]) - parseFloat(embRgb[1])); const bDiff = Math.abs(parseFloat(devRgb[2]) - parseFloat(embRgb[2])); console.log(`RGB Abs Diffs: R=${rDiff.toFixed(2)}, G=${gDiff.toFixed(2)}, B=${bDiff.toFixed(2)}`); const pass = rDiff < 15 && gDiff < 15 && bDiff < 15; console.log(`COLOR ACCURACY PASS: ${pass ? 'YES ✅' : 'NO ❌'}`); } devImg.delete(); if (embImg) embImg.delete(); } catch (err) { console.error(`FAILED ${f}:`, err.message); } }