Edit once, update twice / original
DEVELOPMENT JOURNAL / 32
Photoshot
CODE × UI
Building a free Photoshop alternative

Edit once, update twice

On October 23, 2026, we edited one embedded source in the public Photoshot editor, verified both placed instances and reopened the project and PSD. We also reviewed the separate safeguards used when writing to an external linked file.

PHOTOSHOT DEVELOPMENT JOURNAL / 32

AI-generated demonstration portrait, not an authentic celebrity photograph or official collaboration. Editor captures and outputs were made in Photoshot.

PhotoshotDEVELOPMENT JOURNAL / 32FOUNDATION

From the source.

Source IDs matter more than layer names

Separate layers can share one embedded Smart Object source. smartId reads placedLayer.id; openSmartContents finds matching linkedFiles bytes and opens them as another document.

This joins the existing PSD codec and document compositor to source tracking. The evidence is Photoshot's linking code, rather than an unsupported claim that a new external library was copied.

One contents edit updated both instances

We opened a new AI demonstration PNG, converted it to a Smart Object and duplicated the layer. The layer IDs differed while placedLayer.id matched. In Edit Contents, we entered 75 in Brightness and ran Update Parent Document.

Both parent layers then contained the same changed src, different from the previous source. The instances overlapped at the same position. This portrait does not test every possible placement or transform.

app/studio/smart-objects.ts · SHA-256 8a73282ee8da · L8–10Photoshot implementation ↗
export function smartId(layer:Layer){return layer.psdData?.placedLayer?.id as string|undefined;}
export async function openSmartContents(doc:StudioDoc,layer:Layer):Promise<StudioDoc>{const id=smartId(layer),meta=unpackMetadata(doc.psdData??{}),file=meta.linkedFiles?.find((f:any)=>f.id===id);if(!file?.data?.length)throw Error('This linked Smart Object has no embedded content. Use Replace Contents to select its source file.');return readSmartFile(file.data,file.name??layer.name+'.psd');}
export async function readSmartFile(bytes:Uint8Array,name:string):Promise<StudioDoc>{if(bytes.length>=4&&new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength).getUint32(0)===0x38425053)return readPhotoshop(bytes.slice().buffer,name);const mime=/\.jpe?g$/i.test(name)?'image/jpeg':/\.webp$/i.test(name)?'image/webp':'image/png',blob=new Blob([bytes as BlobPart],{type:mime}),url=URL.createObjectURL(blob);try{const image=await loadImage(url),c=surface(image.width,image.height);c.getContext('2d')!.drawImage(image,0,0);return {...blankDoc(),version:6,name:name.replace(/\.[^.]+$/,''),width:c.width,height:c.height,layers:[makeLayer({kind:'image',name,src:c.toDataURL(),width:c.width,height:c.height})]};}finally{URL.revokeObjectURL(url);}}
PhotoshotDEVELOPMENT JOURNAL / 32IMPLEMENTATION

Inside the
calculation.

Reopen the project and PSD separately

Saving, reopening and saving the parent project produced an exactly equal document. PSD export and reopening retained two layers and the shared source ID. Edit Contents reopened the embedded one-layer document.

updateSmartContents updates layers sharing that ID and adjusts placement and masks to new bounds. Separate rerun fixtures covered conversion, rasterization, placement and PSD/PSB linked writes; those broader fixtures are distinguished from this simple public-editor demonstration.

Writing an external file is a separate operation

writeLinkedContents checks the source key and hash, write permission and locked instances. PSD/PSB output must retain color mode and depth. It prepares encoded bytes and the parent state before opening a write stream, and checks for external changes again before and during the write.

Rerun linked-write checks passed permission denial, stale editing context, external conflicts and failed writes while preserving the file and parent. The code limits linked files to 512 MB, with additional format-specific constraints.

PHOTOSHOT / SOURCE NOTES32

Read the code.

app/studio/smart-objects.ts · SHA-256 8a73282ee8da · L16–17Photoshot implementation ↗
 placed.width=content.width;placed.height=content.height;placed.transform=points.flatMap(p=>[p.x,p.y]);placed.nonAffineTransform=placed.transform.slice();Object.assign(original,{left:bx,top:by,right:bx+bw,bottom:by+bh});const src=c.toDataURL(),precise=preciseContent?positionPrecision(preciseContent,makeLayer({width:bw,height:bh,transformQuad:quad}),bw,bh):undefined;return {...l,x:bx,y:by,width:bw,height:bh,rotation:0,flipX:undefined,flipY:undefined,transformQuad:undefined,mask,vectorMask,smartFilterMask,src,precision:precise?packPrecision(precise):undefined,precisionSource:precise?precisionSource(src):undefined,strokes:[],shape:undefined,typography:undefined,textRuns:undefined,textPath:undefined,textWarp:undefined,raw:undefined,gaussian:undefined,psdData:packMetadata(original)};});return {...doc,version:6,psdData:packMetadata(meta),layers};}
export async function rasterizeSmartObject(doc:StudioDoc,layer:Layer):Promise<StudioDoc>{return rasterizeLayer(doc,layer);}
app/studio/linked-objects.ts · SHA-256 a7d908e96492 · L17–24Photoshot implementation ↗
export async function writeLinkedContents(doc:StudioDoc,layer:Layer,content:StudioDoc,baseline:{key:string;hash:string},handle?:SourceHandle):Promise<StudioDoc>{
 const id=smartId(layer),linked=layer.smartObject?.linked;
 if(!id||!linked)throw Error('This Smart Object is no longer linked.');
 if(linked.key!==baseline.key||linked.hash!==baseline.hash)throw Error('The linked source changed since these contents were opened. Close and reopen Edit Contents before saving.');
 if(doc.layers.some(l=>smartId(l)===id&&layerLocked(doc,l,'pixels')))throw Error('Unlock all instances before saving linked contents.');
 const target=handle??await readLinkedHandle(linked.key,true);
 if(target.queryPermission){let permission=await target.queryPermission({mode:'readwrite'});if(permission!=='granted'&&target.requestPermission)permission=await target.requestPermission({mode:'readwrite'});if(permission!=='granted')throw Error('Write permission was not granted. The source file was not changed.');}
 const check=async()=>{const file=await target.getFile();if(file.size>512*1024*1024)throw Error('Linked source exceeds 512 MB.');const bytes=new Uint8Array(await file.arrayBuffer());if(await checksum(bytes)!==baseline.hash)throw Error('The source file was changed outside Photoshot. Close and reopen Edit Contents after Update Modified Contents.');return {file,bytes};};
app/studio/linked-objects.ts · SHA-256 a7d908e96492 · L44–51Photoshot implementation ↗
 // Build the complete parent state before opening a write stream, so codec failures cannot overwrite the file.
 const next=await updateSmartContents(doc,id,encodedContent),hash=await checksum(bytes);
 const metadata:LinkedSource={...linked,name:file.name,size:bytes.length,modified:Date.now(),hash};
 const result={...next,layers:next.layers.map(l=>smartId(l)===id?{...l,smartObject:{...l.smartObject!,linked:metadata}}:l)};
 await check();
 const stream=await target.createWritable({keepExistingData:false});
 try{await stream.write(bytes as unknown as BufferSource);await check();await stream.close();}catch(error){try{await stream.abort();}catch{}throw error;}
 return result;
PhotoshotDEVELOPMENT JOURNAL / 32IN PRACTICE

In the editor.

Edit once, update twice / workspace
Two layers after Edit Contents → Brightness 75 → parent update.

Undo does not restore the disk file

The linked-source browser check confirmed that parent Undo/Redo does not rewrite a saved PNG. Editing-context recovery after reload and external-change conflict handling also passed.

Precision linked-write fixtures recorded maximum error 0 for RGB32 and about 0.000000954 for RGB16, CMYK16 and Lab16. These are specific fixture results, not a universal compatibility claim. Part 33 follows filter order and masks above the preserved source.

PhotoshotDEVELOPMENT JOURNAL / 32BEFORE / AFTER

Source and result.

Edit once, update twice / original
01 / AI-generated demonstration source
Edit once, update twice / result
02 / Updated pixels shared by both instances

AI-generated demonstration portrait, not an authentic celebrity photograph or official collaboration. Editor captures and outputs were made in Photoshot.