
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.
AI-generated demonstration portrait, not an authentic celebrity photograph or official collaboration. Editor captures and outputs were made in Photoshot.
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.
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);}}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.
Read the code.
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);}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};}; // 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;In the editor.

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.
Source and result.


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