
무료 포토샵 개발기
하나를 고치고 둘을 바꾸다
같은 사진을 두 번 배치한 뒤 원본을 한 번 고쳤습니다. 2026년 10월 23일 공개 포토샷에서 두 인스턴스의 갱신과 프로젝트·PSD 재열기를 확인하고, 외부 연결 파일을 덮어쓸 때 필요한 보호 조건을 코드와 검사로 살폈습니다.
AI 생성 시연 이미지 · 실제 인물 촬영 및 공식 협업 이미지가 아닙니다. 편집 화면과 결과는 실제 포토샷에서 만들었습니다.
출처에서 시작하다.
레이어의 이름보다 원본의 ID가 중요하다
스마트 오브젝트는 사진을 한 번 더 그리는 기능에 그치지 않습니다. 배치마다 다른 레이어 ID가 있어도 같은 내장 원본 ID를 공유할 수 있습니다. smartId는 placedLayer.id를 읽고, openSmartContents는 그 ID에 해당하는 linkedFiles의 바이트를 별도 문서로 엽니다.
이미 마련한 PSD 읽기·쓰기와 문서 합성 코드를 이 관계에 연결합니다. 새 외부 라이브러리를 복사했다고 쓰는 대신, 어떤 원본을 어느 배치가 참조하는지 관리하는 포토샷 코드를 확인했습니다.
한 번의 내부 수정으로 두 배치를 갱신했다
새 AI 시연 PNG를 열어 Convert to Smart Object를 실행하고 레이어를 복제했습니다. 두 레이어의 레이어 ID는 다르지만 placedLayer.id는 같았습니다. Edit Contents로 열린 내부 문서에서 Brightness 입력을 75로 바꾸고 Update Parent Document를 실행했습니다.
부모 문서의 두 레이어는 같은 변경된 src를 갖게 됐고 수정 전 src와는 달랐습니다. 이 예시는 같은 위치에 겹쳐 둔 두 배치의 원본 공유 검사입니다. 서로 다른 위치·변형을 모두 이 사진에서 시험했다는 뜻은 아닙니다.
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);}}계산의
안쪽으로.
프로젝트와 PSD를 각각 다시 열었다
부모 프로젝트를 저장하고 다시 연 뒤 재저장한 문서는 저장 전과 정확히 같았습니다. PSD로 내보내 다시 열었을 때도 두 레이어와 공유 원본 ID가 남았습니다. 다시 Edit Contents를 실행해 내장 문서가 한 레이어로 열리는 것도 확인했습니다.
updateSmartContents는 같은 원본 ID를 가진 레이어를 찾아 갱신하면서 배치와 마스크를 새 경계에 맞춥니다. 별도의 재실행 검사에서는 변환·래스터화·배치 좌표와 PSD·PSB 연결 원본 쓰기까지 확인했습니다. 이번 공개 화면의 간단한 두 인스턴스와 검사용 문서의 범위를 나눠 기록합니다.
외부 파일 쓰기는 부모 문서 갱신과 다르다
연결된 원본을 쓰는 writeLinkedContents는 원본 키와 해시, 쓰기 권한, 잠긴 인스턴스를 검사합니다. PSD·PSB에서는 색상 모드와 비트 심도를 유지하는지 먼저 확인하고, 출력 인코딩과 부모 상태를 준비한 뒤 쓰기 스트림을 엽니다. 쓰기 직전과 도중에도 외부 변경을 다시 검사합니다.
재실행한 linked-write 검사에서는 권한 거부, 오래된 편집 문맥, 외부 변경 충돌과 쓰기 실패가 원본과 부모 상태를 지키는지 통과했습니다. 연결 파일은 현재 코드상 최대 512MB이며, 형식별 저장 제약도 별도로 적용됩니다.
코드를 읽다.
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;편집 화면에서.

Undo가 디스크 파일까지 되돌리지는 않는다
브라우저 연결 원본 검사에서는 PNG를 저장한 뒤 부모 문서 Undo·Redo를 실행해도 외부 파일 픽셀이 다시 쓰이지 않는 것을 확인했습니다. 새로고침 후 편집 문맥 복구와 외부 파일이 바뀐 경우의 충돌 방어도 통과했습니다.
고정밀 연결 쓰기 검사에서는 RGB32 최대 차이 0, RGB16·CMYK16·Lab16 최대 차이 약 0.000000954가 기록됐습니다. 정해진 시험 문서의 결과이며 모든 원본의 완전 호환 수치가 아닙니다. 다음 편은 원본 위의 필터 순서·마스크를 저장하는 문제로 이어갑니다.
원본과 편집 결과.


AI 생성 시연 이미지 · 실제 인물 촬영 및 공식 협업 이미지가 아닙니다. 편집 화면과 결과는 실제 포토샷에서 만들었습니다.