Photoshop 애니메이션 레이어를 SVG로 자동 변환하기

Photoshop 애니메이션 레이어를 SVG로 자동 변환하기

Photoshop에서 작업한 애니메이션 레이어를 SVG 파일로 자동 변환하는 방법을 소개합니다. 이 방법을 통해 웹 애니메이션 제작 workflow를 개선할 수 있습니다.

// @target photoshop
// 현재 열린 PSD 파일의 레이어를 애니메이션 가능한 SVG로 변환

// SVG 헤더와 푸터 템플릿
var SVG_HEADER = '<?xml version="1.0" encoding="UTF-8"?>\n' +
    '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" ';
    
function main() {
    if (!app.documents.length) {
        alert("열린 문서가 없습니다.");
        return;
    }

    var doc = app.activeDocument;
    var docPath = doc.path;
    var docName = doc.name.split('.')[0];
    
    // SVG 저장할 폴더 생성
    var svgFolder = new Folder(docPath + "/" + docName + "_svg");
    if (!svgFolder.exists) {
        svgFolder.create();
    }

    // 문서 크기 정보
    var width = doc.width.value;
    var height = doc.height.value;
    
    // 모든 레이어의 원래 상태 저장
    var originalStates = saveLayerStates(doc.layers);
    
    try {
        // 개별 레이어 SVG 생성
        var layerInfos = processLayers(doc, doc.layers, svgFolder, width, height);
        
        // 애니메이션 SVG 생성
        createAnimatedSVG(layerInfos, svgFolder, width, height, docName);
        
        alert("변환이 완료되었습니다.\n저장 위치: " + svgFolder.fsName);
    } finally {
        // 레이어 상태 복원
        restoreLayerStates(doc.layers, originalStates);
    }
}

function saveLayerStates(layers) {
    var states = [];
    for (var i = 0; i < layers.length; i++) {
        var layer = layers[i];
        if (layer.typename === "LayerSet") {
            states = states.concat(saveLayerStates(layer.layers));
        } else {
            states.push({
                layer: layer,
                visible: layer.visible,
                locked: layer.allLocked
            });
        }
    }
    return states;
}

function restoreLayerStates(layers, states) {
    for (var i = 0; i < states.length; i++) {
        var state = states[i];
        state.layer.visible = state.visible;
        state.layer.allLocked = state.locked;
    }
}

function processLayers(doc, layers, outputFolder, width, height) {
    var layerInfos = [];
    
    for (var i = 0; i < layers.length; i++) {
        var layer = layers[i];
        
        // 레이어 그룹이면 재귀적으로 처리
        if (layer.typename === "LayerSet") {
            layerInfos = layerInfos.concat(processLayers(doc, layer.layers, outputFolder, width, height));
            continue;
        }
        
        // 모든 레이어 숨기기
        hideAllLayers(doc.layers);
        
        // 현재 레이어 잠금 해제 및 표시
        var wasLocked = layer.allLocked;
        layer.allLocked = false;
        layer.visible = true;
        
        // 임시 파일로 PNG 저장
        var tempFile = new File(outputFolder + "/temp.png");
        
        // PNG로 저장
        var pngOpts = new ExportOptionsSaveForWeb();
        pngOpts.format = SaveDocumentType.PNG;
        pngOpts.PNG8 = false;
        pngOpts.transparency = true;
        doc.exportDocument(tempFile, ExportType.SAVEFORWEB, pngOpts);
        
        // PNG를 Base64로 인코딩
        var base64 = pngToBase64(tempFile);
        
        // 개별 레이어 SVG 저장
        var svgContent = SVG_HEADER + 
            'width="' + width + '" height="' + height + '">\n' +
            '<image width="' + width + '" height="' + height + '" ' +
            'xlink:href="data:image/png;base64,' + base64 + '"/>\n' +
            '</svg>';
            
        var svgFile = new File(outputFolder + "/" + layer.name + ".svg");
        svgFile.open('w');
        svgFile.write(svgContent);
        svgFile.close();
        
        // 임시 파일 삭제
        tempFile.remove();
        
        // 레이어 정보 저장
        layerInfos.push({
            name: layer.name,
            base64: base64
        });
        
        // 레이어 상태 복원
        layer.allLocked = wasLocked;
    }
    
    return layerInfos;
}

function createAnimatedSVG(layerInfos, outputFolder, width, height, docName) {
    var duration = 0.5; // 각 레이어당 표시 시간(초)
    var totalDuration = duration * layerInfos.length;
    
    var svgContent = SVG_HEADER + 
        'width="' + width + '" height="' + height + '">\n' +
        '<style>\n' +
        '@keyframes fadeIn {\n' +
        '    from { opacity: 0; }\n' +
        '    to { opacity: 1; }\n' +
        '}\n' +
        '</style>\n';
    
    // 각 레이어 이미지 추가
    for (var i = 0; i < layerInfos.length; i++) {
        var layer = layerInfos[i];
        var delay = i * duration;
        
        svgContent += '<image width="' + width + '" height="' + height + '" ' +
            'xlink:href="data:image/png;base64,' + layer.base64 + '" ' +
            'style="animation: fadeIn ' + duration + 's ease forwards; ' +
            'animation-delay: ' + delay + 's; ' +
            'opacity: 0;"/>\n';
    }
    
    svgContent += '</svg>';
    
    // 애니메이션 SVG 저장
    var animFile = new File(outputFolder + "/" + docName + "_animated.svg");
    animFile.open('w');
    animFile.write(svgContent);
    animFile.close();
}

function hideAllLayers(layers) {
    for (var i = 0; i < layers.length; i++) {
        var layer = layers[i];
        if (layer.typename === "LayerSet") {
            hideAllLayers(layer.layers);
        } else {
            layer.visible = false;
        }
    }
}

function pngToBase64(file) {
    // Base64 인코딩 테이블
    var base64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
    
    // 파일을 바이너리로 읽기
    file.open('r');
    file.encoding = 'BINARY';
    var data = file.read();
    file.close();
    
    var result = '';
    var padding = '';
    var chr1, chr2, chr3;
    var enc1, enc2, enc3, enc4;
    var i = 0;
    
    // 3바이트씩 처리
    while (i < data.length) {
        chr1 = data.charCodeAt(i++);
        chr2 = i < data.length ? data.charCodeAt(i++) : NaN;
        chr3 = i < data.length ? data.charCodeAt(i++) : NaN;
        
        enc1 = chr1 >> 2;
        enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
        enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
        enc4 = chr3 & 63;
        
        if (isNaN(chr2)) {
            enc3 = enc4 = 64;
            padding = '==';
        } else if (isNaN(chr3)) {
            enc4 = 64;
            padding = '=';
        }
        
        result += base64chars.charAt(enc1) +
                 base64chars.charAt(enc2) +
                 base64chars.charAt(enc3) +
                 base64chars.charAt(enc4);
    }
    
    return result;
}

// 스크립트 실행
main();

주요 기능

  1. 레이어 자동 추출
  • 모든 레이어를 개별 SVG 파일로 변환
  • 숨겨진 레이어와 잠긴 레이어도 처리 가능
  • 원본 레이어 이름 유지
  1. 애니메이션 SVG 생성
  • 모든 레이어를 하나의 애니메이션 SVG로 결합
  • 페이드인 효과로 순차적 표시
  • 타이밍 조절 가능

사용 방법

  1. 스크립트 설치
  • JSX 파일을 Photoshop 스크립트 폴더에 복사
  • Windows: C:\Program Files\Adobe\Adobe Photoshop [버전]\Presets\Scripts
  • Mac: /Applications/Adobe Photoshop [버전]/Presets/Scripts
  1. 실행하기
  • Photoshop에서 PSD 파일을 엽니다
  • File > Scripts > ‘PSD to SVG’ 선택
  • 원본 PSD와 같은 위치에 ‘_svg’ 폴더가 생성됨

활용 사례

  1. 웹 애니메이션
  • SVG 애니메이션을 웹사이트에 바로 적용
  • CSS 애니메이션으로 추가 커스터마이징 가능
  1. 인터랙티브 콘텐츠
  • 프레젠테이션 자료 제작
  • 인포그래픽 애니메이션
  1. 모션 그래픽
  • 배너 광고 제작
  • SNS 모션 콘텐츠

장점

  • 시간 절약: 수동으로 레이어를 추출하고 변환하는 시간 단축
  • 일관성: 모든 레이어가 동일한 설정으로 변환
  • 유연성: SVG 형식으로 변환되어 웹 환경에서 활용도 높음
  • 품질 유지: 벡터 기반으로 화질 저하 없음