feat: the camera's own colour, a reading that keeps its place, and cloud links in Settings
- RAW opens at the colour the camera wrote into it: the develop carries the difference against the file's own preview (previewResidual) instead of the per-channel cubic fit, dE00 6.04 -> 0.54; previewMatch.ts keeps only the grid. - Immich reading: keyset position kept across visits, one reading per node, a page in flight retried, timeline buckets for the years on offer. - The wall draws a server frame at the address the proxy serves it (no blob minted for it), and the 80-thumbnail pre-cache on open is gone. - Settings lists the cloud links, flags two that answer at the same address, and forgets one. - Custom frames imported from a file (engine/customFrame.ts, IndexedDB). - Upload an edited frame from Studio to Immich Cloud.
This commit is contained in:
@@ -105,41 +105,50 @@ assert.match(src, /^export function previewWhiteBalance\($/m);
|
||||
// the tags rather than a ratio read off noise.
|
||||
assert.match(src, /if \(count < 4096 \|\| sums\[0\] <= 0 \|\| sums\[1\] <= 0 \|\| sums\[2\] <= 0\) return null;/);
|
||||
|
||||
// The fit has to land on the encoded value in float, not as an 8-bit colour filter
|
||||
// painted over the frame afterwards: the fit carries an exposure, and a channel it
|
||||
// lifted past the white level was cut where it stood — blue first, the channel the
|
||||
// WB gains lift most — which is what left the bright end of the frame short of
|
||||
// blue. Measured on the A5100 frame, pixels at 255: 5.25% blue, 2.10% of them blue
|
||||
// alone through the filter, against 2.46% / 2.44% through this path.
|
||||
// What a RAW opens as is the colour the camera wrote into it. Not fitted: the
|
||||
// develop carries the difference between the file's own preview and this develop's
|
||||
// rendering of the same frame, at the preview's resolution, and adds it back on
|
||||
// the value it left. So there is no curve to come back folded, no body table, and
|
||||
// nothing that can disagree with the file it is opening.
|
||||
//
|
||||
// The curve is pinned at both ends, so white is one of the values it is fitted on
|
||||
// and a blown pixel still lands on white — there is no rolloff after it to pull a
|
||||
// channel back down (the 3x3 this replaced needed one, and it turned every
|
||||
// highlight cyan: 0.0% of the frame reached white on all three channels against
|
||||
// 2.3% now and 3.3% in the camera's preview, dE00 7.1 against 6.6).
|
||||
assert.doesNotMatch(src, /drawMatched|colorMatrix/, 'the fit is painted through a colour filter again');
|
||||
assert.match(code, /float tone\(float4 w, float x\) \{/);
|
||||
assert.match(code, /return clamp\(w\.x \+ x \* \(w\.y \+ x \* \(w\.z \+ x \* w\.w\)\), 0\.0, 1\.0\);/);
|
||||
// Pinned at both ends is not pinned at the plateau. The three curves agree at 1.0
|
||||
// and part company either side of it, and where the frame ran out there is no block
|
||||
// left to fit them together — every blown block is dropped, and every block the
|
||||
// develop blows is dropped too — so the neutral the gate above drew comes apart on
|
||||
// the way out: measured on the Olympus ORF, a 255,255,255 sky came back 254,255,255
|
||||
// (149k px) and 253,255,254 (122k px), red under green across a quarter of the
|
||||
// frame. The gate is read again on the value that leaves, so the pixel the develop
|
||||
// itself called blown leaves at the neutral it was drawn as.
|
||||
assert.match(code, /float3 o = float3\(tone\(t0, e\.r\), tone\(t1, e\.g\), tone\(t2, e\.b\)\);/);
|
||||
// Measured against the file's own preview: dE00 6.04 for the per-channel cubic
|
||||
// this replaced, 0.54 for the residual, and 0.00 on the two bodies (a Nikon NEF
|
||||
// and a DNG) that write a full-size preview — where the two frames agree block for
|
||||
// block and the residual is nothing.
|
||||
const res = src.match(/const RESIDUAL_SKSL = `([\s\S]*?)`;/)?.[1];
|
||||
assert.ok(res, 'RESIDUAL_SKSL not found');
|
||||
const resCode = res.replace(/\/\/[^\n]*/g, '');
|
||||
assert.match(
|
||||
code,
|
||||
/return half4\(half3\(mix\(o, float3\(max\(max\(o\.r, o\.g\), o\.b\)\), blown\)\), 1\.0\);/
|
||||
resCode,
|
||||
/return half4\(half3\(clamp\(ref\.eval\(p\)\.rgb - dev\.eval\(p\)\.rgb \+ 0\.5, 0\.0, 1\.0\)\), 1\.0\);/
|
||||
);
|
||||
assert.match(code, /uniform float4 t0;/);
|
||||
assert.doesNotMatch(code, /float mq = /, 'the rolloff after the fit is back');
|
||||
// ...and it is the tail of the uniform buffer, one float4 per channel, which the
|
||||
// second develop overwrites on the buffer it already built.
|
||||
assert.match(src, /^ *uniforms\.set\(tone \?\? FLAT_TONE, 21\);$/m);
|
||||
assert.match(src, /const uniforms = new Float32Array\(33\);/);
|
||||
assert.match(src, /^import \{ toneMatch, FLAT_TONE, MATCH_GRID \} from '\.\/previewMatch';$/m);
|
||||
assert.match(code, /uniform shader res;/);
|
||||
assert.match(code, /uniform float2 rscl;/);
|
||||
// The 0.5 that keeps a negative difference inside a byte, taken back off on the
|
||||
// way out, and read at the frame's own coordinates — `pos` is local to this band.
|
||||
assert.match(code, /float2 rp = pos \* rscl;/);
|
||||
assert.match(code, /o = clamp\(o \+ \(res\.eval\(rp\)\.rgb - 0\.5\), 0\.0, 1\.0\);/);
|
||||
// The residual is read at the develop's own resolution, so it is filtered up
|
||||
// rather than tapped: one byte a channel, which is finer than the shading it
|
||||
// carries, is the whole difference between two renderings.
|
||||
assert.match(src, /const resChild = \(residual\?\.img \?\? noResidual\(\)\)\.makeShaderOptions\(/);
|
||||
assert.match(src, /const shader = effect\.makeShaderWithChildren\(uniforms, \[child, resChild\]\);/);
|
||||
// A file with nothing to carry opens on a residual that says nothing — 0.5 in
|
||||
// half — rather than down a branch of its own in the shader.
|
||||
assert.match(src, /NO_RESIDUAL = Skia\.Image\.MakeImage\(/);
|
||||
assert.match(src, /new Uint16Array\(\[0x3800, 0x3800, 0x3800, 0x3c00\]\)/);
|
||||
// The develop runs twice, not once: the residual is read off the first frame and
|
||||
// carried by the second.
|
||||
assert.match(src, /^ *const second = develop\(residual\);$/m);
|
||||
// The per-channel curve this replaced is gone, and so is the buffer slot it rode
|
||||
// in: 33 floats became 23, the last two of them the residual's own scale.
|
||||
assert.doesNotMatch(code, /float tone\(float4 w, float x\) \{/, 'the per-channel curve is back');
|
||||
assert.doesNotMatch(code, /uniform float4 t0;/, 'the per-channel curve is back in the shader');
|
||||
assert.doesNotMatch(src, /FLAT_TONE/, 'the flat tone is back');
|
||||
assert.doesNotMatch(src, /toneMatch/, 'the tone fit is back');
|
||||
assert.match(src, /const uniforms = new Float32Array\(23\);/);
|
||||
assert.match(src, /^ *uniforms\[21\] = residual \? residual\.sx : 0;$/m);
|
||||
assert.match(src, /^import \{ MATCH_GRID \} from '\.\/previewMatch';$/m);
|
||||
|
||||
// A RAW opens at the sensor's own resolution, not at the quarter the half-size
|
||||
// demosaic reports: the GR's DNG came back 3010x2012 against the 6000x4000 of the
|
||||
|
||||
Reference in New Issue
Block a user