174 lines
6.8 KiB
JavaScript
174 lines
6.8 KiB
JavaScript
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);
|
|
}
|
|
}
|