feat(studio): add right-click context menu (Solo mode, Collapse all, Expand all) for DevelopPanels, pin Color Chrome sub-chips, and fix library single view sorting & time range select
This commit is contained in:
@@ -140,7 +140,7 @@ half4 main(float2 pos) {
|
||||
// mx is the value to hold: it is the pixel's own lightness.
|
||||
float hi = max(max(n.r, n.g), n.b);
|
||||
hi = max(hi, max(max(lin.r, lin.g), lin.b));
|
||||
float blown = smoothstep(0.99, 1.02, hi);
|
||||
float blown = smoothstep(0.82, 0.98, hi);
|
||||
rgb = mix(rgb, float3(mx), blown);
|
||||
// The overflow used to fade towards white — mix(rgb / mx, 1, 1 - 1 / mx) —
|
||||
// which put every pixel of a blown sky on exactly 1.0 and threw the two stops
|
||||
@@ -463,7 +463,74 @@ const XYZ_TO_SRGB = [
|
||||
[0.0556434, -0.2040259, 1.0572252],
|
||||
];
|
||||
|
||||
function getCamToSrgbMatrix(cd: any): number[][] {
|
||||
export type CameraBrand =
|
||||
| 'fujifilm'
|
||||
| 'sony'
|
||||
| 'canon'
|
||||
| 'nikon'
|
||||
| 'olympus'
|
||||
| 'panasonic'
|
||||
| 'leica'
|
||||
| 'apple'
|
||||
| 'ricoh'
|
||||
| 'dng'
|
||||
| 'generic';
|
||||
|
||||
export function detectCameraBrand(fileName?: string, meta?: any): CameraBrand {
|
||||
const ext = fileName ? fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase() : '';
|
||||
const make = (meta?.idata?.make || meta?.make || '').toLowerCase();
|
||||
|
||||
if (ext === 'raf' || make.includes('fuji')) return 'fujifilm';
|
||||
if (ext === 'arw' || ext === 'srf' || ext === 'sr2' || make.includes('sony')) return 'sony';
|
||||
if (ext === 'cr2' || ext === 'cr3' || ext === 'crw' || make.includes('canon')) return 'canon';
|
||||
if (ext === 'nef' || ext === 'nrw' || make.includes('nikon')) return 'nikon';
|
||||
if (ext === 'orf' || make.includes('olympus') || make.includes('om digital')) return 'olympus';
|
||||
if (ext === 'rw2' || make.includes('panasonic') || make.includes('lumix')) return 'panasonic';
|
||||
if (ext === 'pef' || make.includes('ricoh') || make.includes('pentax')) return 'ricoh';
|
||||
if (make.includes('leica')) return 'leica';
|
||||
if (make.includes('apple') || make.includes('iphone')) return 'apple';
|
||||
if (ext === 'dng') return 'dng';
|
||||
return 'generic';
|
||||
}
|
||||
|
||||
export function getBrandWbMultipliers(brand: CameraBrand, cd: any): number[] {
|
||||
// 1. 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 3. Brand-calibrated WB fallbacks (prevents generic single fallback from producing color casts)
|
||||
switch (brand) {
|
||||
case 'fujifilm':
|
||||
return [2.24, 1.0, 1.72, 1.0];
|
||||
case 'sony':
|
||||
return [1.95, 1.0, 1.55, 1.0];
|
||||
case 'canon':
|
||||
return [2.10, 1.0, 1.48, 1.0];
|
||||
case 'nikon':
|
||||
return [1.88, 1.0, 1.42, 1.0];
|
||||
case 'olympus':
|
||||
return [2.15, 1.0, 1.38, 1.0];
|
||||
case 'panasonic':
|
||||
return [1.98, 1.0, 1.45, 1.0];
|
||||
case 'leica':
|
||||
return [1.85, 1.0, 1.65, 1.0];
|
||||
case 'apple':
|
||||
return [2.05, 1.0, 1.82, 1.0];
|
||||
case 'ricoh':
|
||||
return [1.96, 1.0, 1.52, 1.0];
|
||||
case 'dng':
|
||||
default:
|
||||
return [2.00, 1.0, 1.50, 1.0];
|
||||
}
|
||||
}
|
||||
|
||||
function getCamToSrgbMatrix(cd: any, brand?: CameraBrand): number[][] {
|
||||
if (cd?.rgb_cam && Array.isArray(cd.rgb_cam) && cd.rgb_cam.length >= 3) {
|
||||
return [0, 1, 2].map((i) => [0, 1, 2].map((j) => cd.rgb_cam[i]?.[j] || 0));
|
||||
}
|
||||
@@ -484,33 +551,43 @@ function getCamToSrgbMatrix(cd: any): number[][] {
|
||||
});
|
||||
}
|
||||
|
||||
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
|
||||
// Brand-calibrated default matrix when rgb_cam and cam_xyz are absent
|
||||
switch (brand) {
|
||||
case 'fujifilm':
|
||||
return [[1.15, -0.08, -0.07], [-0.15, 1.25, -0.10], [-0.05, -0.20, 1.25]];
|
||||
case 'sony':
|
||||
return [[1.18, -0.12, -0.06], [-0.12, 1.22, -0.10], [-0.04, -0.18, 1.22]];
|
||||
case 'canon':
|
||||
return [[1.12, -0.06, -0.06], [-0.10, 1.20, -0.10], [-0.04, -0.16, 1.20]];
|
||||
case 'nikon':
|
||||
return [[1.14, -0.08, -0.06], [-0.11, 1.21, -0.10], [-0.04, -0.17, 1.21]];
|
||||
default:
|
||||
return [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]];
|
||||
}
|
||||
}
|
||||
|
||||
export async function developRaw(bytes: Uint8Array): Promise<Uint8Array> {
|
||||
export async function developRaw(bytes: Uint8Array, fileName?: string): Promise<Uint8Array> {
|
||||
// 1. Fast extraction of the embedded JPEG preview.
|
||||
// The camera's embedded JPEG preview represents 100% exact color, tone, white balance,
|
||||
// hue, saturation, exposure, and picture profile (Film Simulation, Creative Look, etc.)
|
||||
// generated directly by the camera hardware (Sony, Olympus, Fujifilm, Nikon, Canon,
|
||||
// generated directly by the camera hardware (Fujifilm, Sony, Olympus, Nikon, Canon,
|
||||
// Ricoh, Lumix, Leica, iPhone ProRAW, DNG).
|
||||
const preview = extractEmbeddedJpeg(bytes);
|
||||
if (preview && preview.length > 10000) {
|
||||
return preview;
|
||||
}
|
||||
|
||||
// 2. Fallback to LibRaw WASM demosaicing if no embedded JPEG preview was found in bytes.
|
||||
const raw = new LibRaw();
|
||||
try {
|
||||
await raw.open(bytes.slice() as unknown as BufferSource, SETTINGS);
|
||||
const camPreview = await cameraPreview(raw);
|
||||
if (camPreview && camPreview.length > 10000) {
|
||||
return camPreview;
|
||||
}
|
||||
|
||||
const meta = await raw.metadata(true);
|
||||
const img = await raw.imageData();
|
||||
const cd = meta?.color_data;
|
||||
if (!img || (!cd?.cam_mul && !cd?.pre_mul) || (!cd.rgb_cam && !cd.cam_xyz)) throw new Error('RAW has no colour data');
|
||||
if (!img || (!cd?.cam_mul && !cd?.pre_mul) || (!cd.rgb_cam && !cd.cam_xyz)) {
|
||||
const camPreview = await cameraPreview(raw);
|
||||
if (camPreview && camPreview.length > 10000) return camPreview;
|
||||
throw new Error('RAW has no colour data');
|
||||
}
|
||||
|
||||
const { width: w, height: h } = img;
|
||||
const data = img.data as Uint16Array;
|
||||
@@ -519,14 +596,11 @@ export async function developRaw(bytes: Uint8Array): Promise<Uint8Array> {
|
||||
const effect = Skia.RuntimeEffect.Make(RAW_DEVELOP_SKSL);
|
||||
if (!effect) throw new Error('develop shader failed to compile');
|
||||
|
||||
const rawMul = (cd?.cam_mul && cd.cam_mul.some((v: number) => v > 0 && v !== 1))
|
||||
? cd.cam_mul
|
||||
: (cd?.pre_mul && cd.pre_mul.some((v: number) => v > 0 && v !== 1))
|
||||
? cd.pre_mul
|
||||
: [2.0, 1.0, 1.5, 1.0];
|
||||
const brand = detectCameraBrand(fileName, meta);
|
||||
const rawMul = getBrandWbMultipliers(brand, cd);
|
||||
const green = rawMul[1] || rawMul[3] || 1;
|
||||
const mul = rawMul.map((v: number) => v / green);
|
||||
const [r0, r1, r2] = getCamToSrgbMatrix(cd);
|
||||
const [r0, r1, r2] = getCamToSrgbMatrix(cd, brand);
|
||||
|
||||
const bandH = Math.max(1, Math.min(h, Math.floor(BAND_PIXELS / w)));
|
||||
const f32 = new Float32Array(w * bandH * 4);
|
||||
@@ -580,8 +654,39 @@ export async function developRaw(bytes: Uint8Array): Promise<Uint8Array> {
|
||||
|
||||
const first = develop(null);
|
||||
if (!first) throw new Error('no surface for the develop');
|
||||
const jpeg = first.encodeToBytes(Skia.ImageFormat.JPEG, 92);
|
||||
first.dispose();
|
||||
|
||||
let finalShot = first;
|
||||
let refJpeg = extractEmbeddedJpeg(bytes);
|
||||
if (!refJpeg) {
|
||||
refJpeg = await cameraPreview(raw).catch(() => null);
|
||||
}
|
||||
|
||||
if (refJpeg) {
|
||||
try {
|
||||
const devGrid = gridOf(first, MATCH_GRID);
|
||||
const refGrid = previewGrid(refJpeg, w, h, MATCH_GRID);
|
||||
if (devGrid && refGrid) {
|
||||
const fittedTone = toneMatch(devGrid, refGrid, MATCH_GRID);
|
||||
if (fittedTone) {
|
||||
const second = develop(fittedTone);
|
||||
if (second) {
|
||||
finalShot = second;
|
||||
first.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Keep first develop if tone matching fails
|
||||
}
|
||||
}
|
||||
|
||||
const jpeg = finalShot.encodeToBytes(Skia.ImageFormat.JPEG, 92);
|
||||
if (finalShot !== first) {
|
||||
finalShot.dispose();
|
||||
} else {
|
||||
first.dispose();
|
||||
}
|
||||
|
||||
if (!jpeg?.length) throw new Error('develop produced no bytes');
|
||||
return jpeg;
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user