파일을 다시 읽다 / original
DEVELOPMENT JOURNAL / 20
Photoshot
CODE × UI
무료 포토샵 개발기

파일을 다시 읽다

무료 포토샵을 만드는 포토샷 개발기입니다. 이번에는 PSD·PSB가 열리는지에서 한 걸음 더 나아가, 다시 저장하고 읽었을 때 어떤 편집 정보가 남는지 살핍니다. 새 AI 인물 사진에 두 레이어를 만들고 두 형식을 각각 왕복했습니다.

PHOTOSHOT DEVELOPMENT JOURNAL / 20

AI 생성 시연용 사진. 편집 화면과 출력 이미지는 실제 포토샷에서 만들었습니다.

Photoshot무료 포토샵 개발기 / 20FOUNDATION

출처에서 시작하다.

파일이 열림과 편집 정보 보존은 다른 기준이다

합성된 사진이 보이더라도 레이어가 한 장으로 합쳐졌다면 원래의 편집 구조는 돌아오지 않은 것입니다. 이름·순서·숨김·불투명도, 마스크·문자·효과 등은 각각 확인해야 합니다. 이번 시연은 그중 두 이미지 레이어의 기본 속성에 집중했습니다.

운영 편집기의 psd-codec.ts는 vendor/psd-codec.mjs의 읽기·쓰기 경로를 사용하면서 포토샷 문서와의 변환을 담당합니다. 외부 형식 해석과 서비스의 문서 모델 사이에는 별도의 연결 코드가 필요합니다. 이 확장 작업본과 공개 저장소의 지원 범위는 구분합니다.

메타데이터를 남기되 픽셀과 문서 구조를 다시 연결한다

psd-metadata.ts는 일반 객체를 재귀적으로 보관하고 바이트 배열은 Base64와 타입 이름으로 감쌉니다. canvas·imageData·rawData·children 같은 항목은 이 메타데이터 사본에서 제외됩니다. 같은 데이터를 무조건 통째로 저장하는 것이 아니라, 다시 만드는 영역과 보관하는 영역을 나누는 구조입니다.

따라서 메타데이터가 남았다는 사실만으로 효과가 화면에 동일하게 그려지거나 모든 문자 기능이 다시 편집된다고 결론 내릴 수 없습니다. psd-resources.ts도 리소스 구간과 길이를 검사합니다. 파일 형식의 정보를 읽는 일과 그 의미를 편집기에 구현하는 일은 별개의 검증 항목입니다.

app/studio/psd-codec.ts · SHA-256 9574fa451bac · L22–26포토샷 구현 코드 ↗
import {readNativeWorkspaces,writeNativeWorkspaces} from './psd-workspaces.ts';
import * as ag from './vendor/psd-codec.mjs';
import {supportedColorModes} from './vendor/psd-codec.mjs';
import instantiate from './vendor/lcms/lcms.js';
import {blankDoc,makeLayer,surface,layerSurface,maskSurface,composite,uid,supportedSize,MAX_LAYERS,type Layer,type StudioDoc,type LayerMask} from './document.ts';
Photoshot무료 포토샵 개발기 / 20IMPLEMENTATION

계산의
안쪽으로.

지원하지 않는 조정은 조용히 바꾸지 않는다

PSD 출력에서 조정 레이어를 대응할 수 없으면 코드가 오류를 내고 포토샷 프로젝트로 보존하라는 안내를 만듭니다. 이름만 비슷한 효과로 바꿔 저장한 뒤 완전 호환이라고 쓰지 않기 위한 경계입니다. 편집 가능한 원본을 .layerstudio로 함께 남기는 이유이기도 합니다.

PSB라는 확장자를 고른다고 앱의 작업 공간 제한이 사라지지도 않습니다. 읽기 경로에는 16,384px·64MP 범위 검사가 있으며, 고정밀 처리에는 별도 제약이 있습니다. 파일 형식의 최대 규격과 현재 앱에서 처리하는 범위를 혼동하지 않아야 합니다.

PSD와 PSB 모두 두 레이어를 다시 확인했다

2026년 10월 22일 1536×1024 AI 원본에 Portrait base와 Hidden backup 60 두 레이어를 만들었습니다. 위쪽 복제 레이어는 불투명도 60%, 숨김 상태입니다. 프로젝트 원본을 보관한 뒤 File → Save As PSD와 Save As PSB를 각각 실행하고 Open PSD / PSB로 다시 열었습니다.

두 파일 모두 이름·레이어 순서·숨김·불투명도 0.6이 유지됐습니다. 내려받은 파일의 헤더도 확인해 PSD 버전 1, PSB 버전 2, 같은 크기와 8비트를 확인했습니다. 오늘의 실측은 포토샷에서 저장하고 포토샷으로 다시 읽는 경로입니다. 오늘 Adobe Photoshop에서 직접 다시 열었다는 결과와 섞지 않습니다.

PHOTOSHOT / SOURCE NOTES20

코드를 읽다.

app/studio/psd-metadata.ts · SHA-256 1e4349e5ee5e · L3–4포토샷 구현 코드 ↗
export function packMetadata(v:any):any{if(v===null||typeof v!=='object')return v;if(ArrayBuffer.isView(v))return {__bytes:bytes64(new Uint8Array(v.buffer,v.byteOffset,v.byteLength)),__type:v.constructor.name};if(v instanceof ArrayBuffer)return {__bytes:bytes64(new Uint8Array(v)),__type:'ArrayBuffer'};if(Array.isArray(v))return v.map(packMetadata);const out:any={};for(const [k,x]of Object.entries(v))if(!['canvas','imageData','rawData','rawCompositeData','children','thumbnail','imageDataRaw'].includes(k))out[k]=packMetadata(x);return out;}
export function unpackMetadata(v:any):any{if(!v||typeof v!=='object')return v;if(v.__bytes){const a=from64(v.__bytes);return v.__type==='ArrayBuffer'?a.buffer:v.__type==='Uint16Array'?new Uint16Array(a.buffer):v.__type==='Float32Array'?new Float32Array(a.buffer):a;}if(Array.isArray(v))return v.map(unpackMetadata);return Object.fromEntries(Object.entries(v).map(([k,x])=>[k,unpackMetadata(x)]));}
app/studio/psd-resources.ts · SHA-256 405e8d1a13ef · L2–4포토샷 구현 코드 ↗
export function resourceRange(bytes:Uint8Array){const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),lengthOffset=30+v.getUint32(26),start=lengthOffset+4,end=start+v.getUint32(lengthOffset);if(end>bytes.length)throw Error('Truncated PSD image resources.');return {lengthOffset,start,end};}
export function resourceBlocks(bytes:Uint8Array){const {start,end}=resourceRange(bytes),v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength),out:{id:number;start:number;end:number;data:Uint8Array}[]=[];let at=start;while(at+12<=end){const first=at,id=v.getUint16(at+4),name=bytes[at+6];at+=6+Math.ceil((name+1)/2)*2;if(at+4>end)throw Error('Invalid PSD resource name.');const length=v.getUint32(at);at+=4;if(at+length>end)throw Error('Invalid PSD resource length.');const data=bytes.subarray(at,at+length);at+=length+(length%2);out.push({id,start:first,end:at,data});}return out;}
export function readResource(bytes:Uint8Array,id:number){return resourceBlocks(bytes).find(b=>b.id===id)?.data;}
app/studio/psd-codec.ts · SHA-256 9574fa451bac · L63–63포토샷 구현 코드 ↗
 if(l.adjustment){const mapped=adjustmentOut(l.adjustment,out.adjustment);if(mapped){out.adjustment=l.psdAdjustmentOriginal===JSON.stringify(l.adjustment)?out.adjustment:mapped;out.clipping=l.adjustment.scope==='clipped';}else throw Error('PSD export does not yet support '+l.adjustment.type+' adjustment layers. Save a Photoshot project to preserve them.');}
Photoshot무료 포토샵 개발기 / 20IN PRACTICE

편집 화면에서.

파일을 다시 읽다 / workspace
실제 포토샷 작업 화면. 설정과 사진을 함께 확인할 수 있습니다.

검증된 시험 파일과 모든 파일을 구분한다

보관된 precision-psd-render 보고서는 RGB16·RGB32·CMYK16·Lab16의 PSD/PSB 여덟 조합을 통과로 기록합니다. 파일 날짜는 2026년 10월 13일입니다. 이 과거 엔진 보고서를 오늘 새로 실행한 검사로 표현하지 않으며, 인물 사진의 8비트 기본 레이어 시연과도 구분합니다.

기능 대조표에도 다양한 문자 스타일·효과 조합과 전체 Adobe 렌더 동등성 등 남은 범위가 적혀 있습니다. 시연 인물은 AI 생성 모델이고 작업 화면은 실제 편집기 캡처입니다. 다음 21편은 레이어 그룹·효과·마스크를 어떤 문서 구조로 함께 관리하는지 이어서 살펴봅니다.

Photoshot무료 포토샵 개발기 / 20BEFORE / AFTER

원본과 PSD 재열기.

파일을 다시 읽다 / original
01 / AI 생성 시연 원본
파일을 다시 읽다 / result
02 / PSD 재열기 · 두 레이어와 숨김·60% 유지

AI 생성 시연용 사진. 편집 화면과 출력 이미지는 실제 포토샷에서 만들었습니다.