Keep values beyond the display / original
DEVELOPMENT JOURNAL / 30
Photoshot
CODE × UI
Building a free Photoshop alternative

Keep values beyond the display

We converted a portrait to 32-bit RGB and doubled its brightness in Photoshot. Actual stored data shows whether values survive a clipped-looking preview, PSD export and reopening, and return after reversing the exposure.

PHOTOSHOT DEVELOPMENT JOURNAL / 30

AI-generated study photograph. Editor captures and outputs show Photoshot in use.

PhotoshotDEVELOPMENT JOURNAL / 30FOUNDATION

From the source.

Give higher bit depth a concrete purpose

The input is a 1536×1024 8-bit PNG of an AI-generated fictional adult. Converting it to 32-bit cannot create detail absent from the source. It can retain finer results of subsequent calculations and brightness values outside the display range.

precision-raster.ts stores working pixels in Float32Array and converts them for screen display separately. RGB and Gray support 32-bit; Lab and CMYK use 8 or 16 bits. Channel counts, alpha and finite values are validated, rather than merely changing a bit-depth label.

Apply a real +1 EV precision adjustment

On October 22, 2026, we used Image > Color Mode and Precision to convert the image to RGB, 32 bits/channel. After saving a baseline project, Precision Channels used Exposure 1, Gamma 1, Offset 0, with every channel Gain 1 and Offset 0. We previewed and applied it.

Decoded project data showed RGB values exactly twice the baseline, with unchanged alpha. There were 350,081 RGB channel values above 1, and the maximum was approximately 1.964501. Some areas look clipped to white on screen, but the stored floating-point values were not clamped to 1.

app/studio/precision-raster.ts · SHA-256 11f562b76d03 · L5–12Photoshot implementation ↗
export type PrecisionSpace='RGB'|'CMYK'|'Lab'|'Gray';export type PrecisionDepth=8|16|32;
export type PrecisionRaster={width:number;height:number;space:PrecisionSpace;depth:PrecisionDepth;data:Float32Array;profile?:Uint8Array};
export type PackedPrecision={version:1;width:number;height:number;space:PrecisionSpace;depth:PrecisionDepth;data:string;profile?:string};
export const precisionChannels=(space:PrecisionSpace)=>space==='CMYK'?4:space==='Gray'?1:3;
export function validatePrecision(r:PrecisionRaster){const n=precisionChannels(r.space);if(!['RGB','CMYK','Lab','Gray'].includes(r.space)||![8,16,32].includes(r.depth)||r.depth===32&&!['RGB','Gray'].includes(r.space)||!Number.isInteger(r.width)||!Number.isInteger(r.height)||r.width<1||r.height<1||r.width>16384||r.height>16384||r.width*r.height>16000000||r.data.length!==r.width*r.height*(n+1))throw Error('Invalid precision raster or unsupported size (16 MP maximum).');for(let i=0;i<r.data.length;i++)if(!Number.isFinite(r.data[i])||Math.abs(r.data[i])>65504||i%(n+1)===n&&(r.data[i]<0||r.data[i]>1))throw Error('Invalid high-precision pixel.');if(r.profile){if(r.profile.length<132||r.profile.length>16*1024*1024)throw Error('Invalid ICC profile size.');const v=new DataView(r.profile.buffer,r.profile.byteOffset,r.profile.byteLength),sig=(at:number)=>String.fromCharCode(...r.profile!.subarray(at,at+4));if(sig(36)!=='acsp'||v.getUint32(0)!==r.profile.length||sig(16)!==({RGB:'RGB ',CMYK:'CMYK',Lab:'Lab ',Gray:'GRAY'})[r.space])throw Error('ICC profile does not match the pixel color mode.');}}
const encode=(bytes:Uint8Array)=>{let s='';for(let i=0;i<bytes.length;i+=8192)s+=String.fromCharCode(...bytes.subarray(i,i+8192));return btoa(s);};const decode=(s:string)=>Uint8Array.from(atob(s),c=>c.charCodeAt(0));
const cache=new WeakMap<PackedPrecision,PrecisionRaster>();
export function packPrecision(r:PrecisionRaster):PackedPrecision{validatePrecision(r);const result:PackedPrecision={version:1,width:r.width,height:r.height,space:r.space,depth:r.depth,data:encode(deflate(new Uint8Array(r.data.buffer,r.data.byteOffset,r.data.byteLength))),...(r.profile?{profile:encode(r.profile)}:{})};cache.set(result,{...r,data:r.data.slice()});return result;}
PhotoshotDEVELOPMENT JOURNAL / 30IMPLEMENTATION

Inside the
calculation.

Reopen PSD before reversing the exposure

Save As PSD produced a file whose header identifies RGB and 32-bit depth. We reopened that actual file using Open PSD / PSB, saved a project and compared every channel. The maximum difference from the pre-export values was zero.

Applying Precision Exposure −1 after reopening restored every RGB and alpha value exactly to the 32-bit baseline. We verified data through the file round trip rather than judging information loss from a bright preview. This result specifically covers the tested +1 EV and −1 EV operations.

Measure Lab conversion and 16-bit quantization

Switching the restored document to Lab selected 16-bit precision. We ran Preview and OK, saved both a project and a PSD, and reopened the PSD. The editor reported Lab 16-bit, and the file header also identified Lab mode.

Lab channels are stored in normalized form internally. The maximum channel difference through PSD was approximately 0.00000763; the mean was approximately 0.00000287. This lies within 16-bit integer quantization when values are written and read back as Float32. It is not a perceptual color-difference score.

PHOTOSHOT / SOURCE NOTES30

Read the code.

app/studio/precision-raster.ts · SHA-256 11f562b76d03 · L14–18Photoshot implementation ↗
const decodeRGB=(x:number)=>x<=.04045?x/12.92:((x+.055)/1.055)**2.4,encodeRGB=(x:number)=>x<=.0031308?12.92*x:1.055*Math.pow(x,1/2.4)-.055;
export type PrecisionAdjustment={exposure:number;gamma:number;offset:number;channels:{gain:number;offset:number}[]};
export function adjustPrecision(r:PrecisionRaster,a:PrecisionAdjustment,selection?:ArrayLike<number>){validatePrecision(r);const n=precisionChannels(r.space),stride=n+1;if(!Number.isFinite(a.exposure)||Math.abs(a.exposure)>20||!Number.isFinite(a.gamma)||a.gamma<.01||a.gamma>10||!Number.isFinite(a.offset)||Math.abs(a.offset)>1||a.channels.length!==n||a.channels.some(c=>!Number.isFinite(c.gain)||c.gain<0||c.gain>8||!Number.isFinite(c.offset)||Math.abs(c.offset)>1)||selection&&selection.length!==r.width*r.height)throw Error('Invalid precision adjustment.');const out={...r,data:r.data.slice()};for(let i=0;i<out.data.length;i+=stride){const amount=selection?Math.max(0,Math.min(1,selection[i/stride]/255)):1;if(!amount)continue;for(let c=0;c<n;c++){const original=r.data[i+c];let v=original;if(r.space==='RGB'&&r.depth!==32)v=decodeRGB(v);if(r.space!=='Lab'||c===0){const value=v*2**a.exposure+a.offset;v=Math.sign(value)*Math.pow(Math.abs(value),1/a.gamma);}if(r.space==='RGB'&&r.depth!==32)v=encodeRGB(v);v=v*a.channels[c].gain+a.channels[c].offset;if(r.depth!==32)v=Math.max(0,Math.min(1,v));out.data[i+c]=original+(v-original)*amount;}}validatePrecision(out);return out;}
export async function convertPrecision(r:PrecisionRaster,space:PrecisionSpace,depth:PrecisionDepth,profile?:Uint8Array,wasmBinary?:Uint8Array,options:{intent?:0|1|2|3;blackPoint?:boolean}={}):Promise<PrecisionRaster>{validatePrecision(r);if(options.intent!==undefined&&![0,1,2,3].includes(options.intent))throw Error('Invalid color rendering intent.');if(depth===32&&!['RGB','Gray'].includes(space))throw Error('32-bit editing supports RGB and Grayscale.');if(r.space===space&&(r.profile===profile||r.profile&&profile&&r.profile.length===profile.length&&r.profile.every((v,i)=>v===profile[i]))){const out={...r,depth,data:r.data.slice()};if(['RGB','Gray'].includes(r.space)&&!r.profile&&r.depth!==depth&&(r.depth===32||depth===32)){const n=precisionChannels(r.space)+1;for(let i=0;i<out.data.length;i++)if(i%n!==n-1)out.data[i]=depth===32?decodeRGB(out.data[i]):encodeRGB(out.data[i]);}if(depth!==32)for(let i=0;i<out.data.length;i++)out.data[i]=Math.max(0,Math.min(1,out.data[i]));return out;}
 if(r.space==='CMYK'&&!r.profile){const data=new Float32Array(r.width*r.height*4);for(let i=0;i<r.width*r.height;i++){for(let c=0;c<3;c++)data[i*4+c]=(1-r.data[i*5+c])*(1-r.data[i*5+3]);data[i*4+3]=r.data[i*5+4];}return convertPrecision({...r,space:'RGB',data},space,depth,profile,wasmBinary,options);}
app/studio/precision-psd.ts · SHA-256 8790a9751d9d · L6–10Photoshot implementation ↗
export function decodePrecisionPlane(data:Uint8Array,compression:number,width:number,height:number,depth:PrecisionDepth,large=false,inflated=false){const bytes=depth/8,size=width*height;if(size>16000000||!Number.isInteger(size)||width<0||height<0)throw Error('Invalid precision channel dimensions.');let raw:Uint8Array;if(compression===0){if(data.length<size*bytes)throw Error('Truncated precision channel.');raw=data.slice(0,size*bytes);}else if(compression===1)raw=decodePlane(data,1,width*bytes,height,large);else if((compression===2||compression===3)&&inflated){if(data.length!==size*bytes)throw Error('Invalid inflated precision plane.');raw=data.slice();}else if(compression===2||compression===3){raw=new Uint8Array(size*bytes);let offset=0;const stream=new Inflate();stream.onData=(chunk:Uint8Array)=>{if(offset+chunk.length>raw.length)throw Error('Oversized compressed precision channel.');raw.set(chunk,offset);offset+=chunk.length;};stream.push(data,true);if(stream.err||offset!==raw.length)throw Error('Invalid compressed precision channel length.');}else throw Error('Unsupported precision compression.');const out=new Float32Array(size),v=new DataView(raw.buffer,raw.byteOffset,raw.byteLength);if(compression===3&&depth===32){for(let y=0;y<height;y++){const start=y*width*4;for(let i=1;i<width*4;i++)raw[start+i]=(raw[start+i]+raw[start+i-1])&255;const tmp=new Uint8Array(width*4);for(let x=0;x<width;x++)for(let b=0;b<4;b++)tmp[x*4+b]=raw[start+b*width+x];raw.set(tmp,start);}}for(let y=0;y<height;y++){let previous=0;for(let x=0;x<width;x++){const i=y*width+x;let value=depth===8?raw[i]:depth===16?v.getUint16(i*2):v.getFloat32(i*4);if(compression===3&&depth!==32){value=(value+previous)%(depth===8?256:65536);previous=value;}out[i]=depth===32?value:value/(depth===16?65535:255);}}return out;}
export function precisionLayerPixels(layer:any,space:PrecisionSpace,depth:PrecisionDepth,profile?:Uint8Array):PrecisionRaster|undefined{const width=(layer.right??0)-(layer.left??0),height=(layer.bottom??0)-(layer.top??0);if(!width||!height||!layer.rawData)return;const n=precisionChannels(space),data=new Float32Array(width*height*(n+1));for(let i=n;i<data.length;i+=n+1)data[i]=1;for(const channel of layer.rawData.channels){const id=channel.id;if(!channel.data||id< -1||id>=n)continue;const plane=decodePrecisionPlane(channel.data,channel.compression,width,height,depth,!!layer.rawData.large),index=id===-1?n:id;for(let i=0;i<plane.length;i++)data[i*(n+1)+index]=space==='CMYK'&&id>=0?1-plane[i]:plane[i];}const result:PrecisionRaster={width,height,space,depth,data,...(profile?{profile}:{})};validatePrecision(result);return result;}
export function readPrecisionPSD(bytes:Uint8Array){const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),mode=view.getUint16(24),space=({1:'Gray',3:'RGB',4:'CMYK',9:'Lab'} as const)[mode as 1|3|4|9],depth=view.getUint16(22) as PrecisionDepth;if(!space||![8,16,32].includes(depth)||depth===32&&!['RGB','Gray'].includes(space))throw Error('Unsupported native document color mode.');if(!ag.supportedColorModes.includes(mode))ag.supportedColorModes.push(mode);const psd=ag.readPsd(bytes,{useRawData:true,skipCompositeImageData:true,skipThumbnail:true,totalMemoryLimit:768*1024*1024}),profile=readResource(bytes,1039);return {psd,space,depth,profile};}
const join=(parts:Uint8Array[])=>{const out=new Uint8Array(parts.reduce((n,b)=>n+b.length,0));let at=0;for(const b of parts){out.set(b,at);at+=b.length;}return out;},word=(n:number,bytes=4)=>{const b=new Uint8Array(bytes),v=new DataView(b.buffer);if(bytes===8)v.setBigUint64(0,BigInt(n));else if(bytes===2)v.setUint16(0,n);else v.setUint32(0,n);return b;};
export function channelBytes(r:PrecisionRaster,id:number){const n=precisionChannels(r.space),stride=n+1,bpp=r.depth/8,bytes=new Uint8Array(r.width*r.height*bpp),view=new DataView(bytes.buffer);for(let i=0;i<r.width*r.height;i++){let v=r.data[i*stride+(id===-1?n:id)];if(r.space==='CMYK'&&id>=0)v=1-v;if(r.depth===32)view.setFloat32(i*4,v);else if(r.depth===16)view.setUint16(i*2,Math.round(Math.max(0,Math.min(1,v))*65535));else bytes[i]=Math.round(Math.max(0,Math.min(1,v))*255);}return bytes;}
app/studio/precision-document.ts · SHA-256 1cb4542a3a67 · L40–41Photoshot implementation ↗
export async function convertDocumentPrecision(doc:StudioDoc,space:PrecisionSpace,depth:PrecisionDepth,profile?:Uint8Array,wasmBinary?:Uint8Array){if(doc.duotone){const c=await composite(doc,true);doc={...doc,duotone:undefined,precision:undefined,groups:undefined,artboards:undefined,layers:[makeLayer({kind:'image',name:'Converted Duotone',width:doc.width,height:doc.height,src:c.toDataURL()})]};}const working:PrecisionDocument={space,depth,profile:profileString(profile)},layers:Layer[]=[];for(const l of doc.layers){if(l.kind==='adjustment'){layers.push(l);continue;}const source=doc.precision&&!(space==='RGB'&&depth===8&&l.raw)?await precisionLayer(l,doc.precision,{masks:false,wasmBinary}):floatCanvas(await layerSurface({...l,mask:undefined,vectorMask:undefined})),pixels=await convertPrecision(source,space,depth,profile,wasmBinary),src=(await precisionCanvas(pixels,wasmBinary)).toDataURL();layers.push(bakedPrecisionLayer(l,pixels,src));}const next:StudioDoc={...doc,version:6 as const,precision:working,layers};if(space==='RGB'&&depth===8&&!profile){next.precision=undefined;next.layers=layers.map(l=>({...l,precision:undefined,precisionSource:undefined}));}else await compositePrecisionDocument(next,wasmBinary);return next;}
export async function adjustPrecisionLayer(doc:StudioDoc,l:Layer,a:PrecisionAdjustment,wasmBinary?:Uint8Array){if(!doc.precision)throw Error('Choose Image > Color Mode and Precision first.');const r=await precisionLayer(l,doc.precision,{masks:false,wasmBinary}),selection=doc.selection?selectionForLayer(doc.selection,l):undefined,edited=adjustPrecision(r,a,selection?.alpha),src=(await precisionCanvas(edited,wasmBinary)).toDataURL();return{...doc,layers:doc.layers.map(x=>x.id===l.id?bakedPrecisionLayer(x,edited,src):x)};}
PhotoshotDEVELOPMENT JOURNAL / 30IN PRACTICE

In the editor.

Keep values beyond the display / workspace
Actual +1 EV in RGB 32-bit. Bright stored values exceed 1.

Profiles and masks remain part of the precision story

Color conversion uses the LittleCMS path in precision-raster.ts. We directly ran default Lab conversion and the 32-bit PSD round trip. Conversion into a user CMYK working space needs a suitable CMYK ICC profile; that is a different purpose from Abstract or Device Link profiles used by Color Lookup.

High-bit color pixels do not imply equally precise auxiliary data. Current mask paths can apply alpha from a display raster, which must be distinguished from 32-bit color-channel preservation. This experiment uses one original layer to isolate the values. Part 31 continues with actual print, separation and duotone output.

PhotoshotDEVELOPMENT JOURNAL / 30BEFORE / AFTER

Original and recovered exposure.

Keep values beyond the display / original
01 / AI-generated demonstration original
Keep values beyond the display / result
02 / Reopened PSD, then −1 EV · baseline values restored

AI-generated study photograph. Editor captures and outputs show Photoshot in use.