LINE Engineering
Blog

웹 브라우저와 함께 하는 애니메이션 스티커 체커

ha1f 2018.05.28

LINE Fukuoka에서 iOS 앱 개발을 담당하고 있습니다.

안녕하세요, LINE Fukuoka의 ha1f입니다. 현재 후쿠오카에서 iOS 앱 개발을 담당하고 있습니다. 저는 LINE에 정식으로 입사하기 전, 채용이 확정된 아르바이트생으로서 LINE의 사내 도구를 개발해 왔습니다. 오늘은 당시 개발한 도구 중 하나인 애니메이션 스티커 체커*에 대해 소개할까 합니다.

APNG란?

APNG(Animated Portable Network Graphics)란 일련 번호가 매겨진 애니메이션용 이미지 파일 형식으로, LINE 애니메이션 스티커에서도 이 형식을 사용하고 있습니다. APNG는 GIF와 달리 풀컬러를 사용할 수 있고 알파 채널을 가질 수도 있으며, 압축율이 높다는 장점이 있습니다. 또한 PNG와 호환되며, APNG를 지원하지 않는 환경에서는 일반적인 정지 화면으로 표시됩니다. 도구를 사용하면 여러 PNG 파일에 일련 번호를 부여하여 APNG 파일을 만들 수도 있습니다.

apng

애니메이션 스티커 체커

LINE의 애니메이션 스티커는 다음과 같은 몇 가지 조건을 충족시켜야 합니다. 자세한 내용을 알기 원하시는 분은 LINE Creators Market에 명시된 제작 가이드라인을 참고하세요.

  • 크기
    • 가로 320px, 세로 270px 이내
    • 가로, 세로 중 한쪽은 반드시 270px 이상
  • 재생 시간: 스티커 하나 당 최대 4초
  • 프레임 수: 5~20 프레임
  • 컬러 모드: RGB
  • 용량: 파일 하나 당 300KB 이하

애니메이션 스티커 체커*는 제작된 스티커가 위에 명시한 조건대로 만들어졌는지 확인할 때 보조적으로 사용할 수 있는 도구입니다. 아직 LINE 외부에 공개되지는 않았습니다. 다음은 애니메이션 스티커 체커를 이용하는 모습을 담은 이미지입니다. 참고로 스티커 자체의 이미지는 보이지 않도록 처리했습니다.

ASC

이미지가 위치한 폴더를 애니메이션 스티커 체커에 마우스로 끌어다 놓기만 하면, 이미지가 재귀적으로 로딩되어 이미지의 크기, 용량, 컬러 모드 등의 정보가 화면에 나타나며, 문제가 있다면 에러도 표시됩니다. 이번 릴리스에서는 Mac과 Windows 양쪽을 모두 지원하기 위해 웹 브라우저 기반의 SPA(Single Page Application)의 형태로 개발했습니다.

웹 브라우저에서 APNG 재생하기

APNG가 지원되는 웹 브라우저를 사용할 때는 <img> 태그의 src 속성에 파일 경로를 설정해두기만 하면 APNG 파일은 자동으로 재생됩니다. 예전에는 Firefox에서만 지원되었으나, 2014년에 Safari에서도 APNG 지원을 시작했고, 2017년 6월부터는 Chrome도 지원하고 있습니다. 지원되지 않는 웹 브라우저에서 APNG 파일을 재생해야 한다면, davidmz/apng-canvas 같은 라이브러리를 사용해 이미지를 표현할 수 있습니다. apng-canvas 라이브러리 내부에서는 바이너리를 읽어들여 분석한 뒤, 프레임별로 캔버스에 띄웁니다.

이미지의 기본 정보 알아내기

애니메이션 스티커가 조건에 따라 만들어졌는지 확인하려면, 먼저 스티커에 담긴 이미지의 정보를 알아내야 합니다. 먼저 이미지 크기에 대한 정보를 알아볼까요? 이미지의 크기는 우리가 아는 일반적인 방법으로 구할 수 있습니다. 다음의 예제 코드를 확인해 보세요. 하지만 이미지 파일 용량이나 컬러 모드 정보는 다음의 방법으로는 알 수 없기 때문에 별도의 구현이 필요합니다.

var img = $("<img />", {
    alt: foldername + "/" + filename
});
img.bind('load', function() {
    const dom = img.get(0);
    const width = dom.naturalWidth;
    const height = dom.naturalHeight;
}
img.attr({ src: e.target.result });

File API로 파일 정보 알아내기

APNG 파일에 대해 크기 외에 추가 정보를 확인하는 방법에 대해 알아보겠습니다. Web API 중의 하나인 File API를 이용할 건데요, File API가 유용한 점은 로컬에 위치한 파일을 JavaScript로 로딩할 수 있는 점입니다. 때문에 파일을 서버로 업로드하기 전이라도 로컬에서 이미지를 미리 볼 수 있고 이미지 파일에 대한 분석이 가능합니다. File API를 이용하여 이미지 정보를 더욱 세밀하게 분석해 보겠습니다.

apng-canvas는 XMLHttpRequest를 사용합니다.

FileReader, File, FileList

HTML 코드에 <input type="file">이라는 요소만 있으면 File API를 사용할 수 있습니다. 이 요소에 다음과 같이 디렉터리 속성을 추가하면 디렉터리 선택이 가능해집니다. 다만, 표준화된 것은 아니기 때문에 Chrome 등 일부 웹 브라우저에서만 디렉터리 선택이 가능합니다.

<input type="file" webkitdirectory directory>

다음은 파일의 크기를 알아내는 코드입니다. 먼저, FileList 객체가 이벤트 핸들러에 전달되면 루프 구문을 통해 APNG를 구성하고 있는 각 이미지에 대한 File 객체를 꺼내 FileReader를 이용하여 파일을 분석합니다. File 객체는 Blob 객체를 상속한 것으로, 파일명이나 크기, MIME 타입 등을 취득할 수 있어 웹 브라우저가 처리해야 하는 대상의 범위를 좁힐 수 있습니다.

handleDirectorySelected(e) {
    Array.from(e.target.files).forEach((file, index, array) => {
        const fileSize = file.size;
        // 필요 시 name이나 type 등을 이용해 로딩 대상의 범위를 좁힘.
        const reader = new FileReader();
        reader.onload = (e) => { /* some process */  }
        reader.readAsArrayBuffer(file);
    });
}

ArrayBuffer와 DataView로 파일 읽기

먼저, FileReader 인터페이스를 이용해서 읽어들인 이미지 파일 데이터가 텍스트 형식이나 Data URI 형식으로 표시되도록 지정할 수도 있지만, 애니메이션 스티커 체커에서는 바이너리 분석에 편리한 ArrayBuffer 객체를 사용했습니다. ArrayBuffer는 길이가 고정된 바이너리를 담는 버퍼이며, 이 버퍼는 담은 데이터를 직접적으로 조작할 수 없는 데이터 타입입니다. 따라서 버퍼를 단독으로는 사용할 수 없으므로, DataView를 이용해 이미지 파일을 읽어들입니다.

const fileReader = new FileReader();
fileReader.onload = function(e) {
    const arrayBuffer = e.target.result;
    const dataView = new DataView(arrayBuffer, 0);
    // dataView로 처리함.
}
fileReader.readAsDataURL(file);

이제 차례대로 파일에 대한 정보를 읽어 보겠습니다.

ArrayBuffer에 담긴 이미지 보여주기

스티커를 구성하고 있는 이미지를 애니메이션 스티커 체커에 표시하려면, ArrayBuffer에 담은 이미지 바이너리를 웹 브라우저에 표시해야 합니다. 통상적으로 다음과 같이 바이너리를 Data URI 형식으로 변환하면 이미지를 쉽게 표시할 수 있습니다.

img.attr({ src: `data:image/png;base64,${btoa(Array.from(new Uint8Array(this.arrayBuffer), e => String.fromCharCode(e)).join(''))}` });

PNG와 APNG의 청크 구조 이용해서 이미지 정보 읽기

PNG의 사양은 ISO/IEC 15948:2003로 정의되어 있으며, Portable Network Graphics(PNG) Specification(Second Edition)에서 확인하실 수 있습니다. PNG의 바이너리는 시그니처인 89 50 4E 47 0D 0A 1A 0A라는 8 바이트 문자열로 시작되며, 그 뒤로 청크라 불리는 데이터 덩어리가 여러 개 이어집니다. 청크의 종류는 다양합니다. 가령 'IHDR' 청크에는 이미지 크기나 컬러 타입이, 'IDAT' 청크에는 이미지 데이터가 포함됩니다.

청크 이름 청크 크기(바이트)
PNG 시그니쳐 8
IHDR 청크 25
... ...
IDAT 청크 가변적
... ...
IEND 청크 12

각각의 청크는 다음의 구조로 이루어져 있습니다. 참고로 "청크 크기" 필드 값이 0이면, "청크 데이터" 필드가 생략됩니다.

청크 필드 필드 값 크기(바이트)
청크 크기 4
청크의 종류 4
청크 데이터 "청크 크기" 필드의 값과 동일함
CRC 4

다음의 코드와 같이, 이미지 파일의 청크를 순서대로 읽어가다 보면 청크 배열을 얻을 수 있습니다. 위 표의 "청크의 종류" 필드 값은 Chunk layout에 따라 ISO 646에 정의된 문자로만 이루어지며, 문자열로 변환해서 저장하고 있습니다. 아래 코드에서는 생략되었지만, `readChunk()` 메서드는 지정 위치를 기준으로 청크 하나 만큼의 데이터를 읽어들이는 메서드입니다.

getChunks() {
    const chunks = [];
    let chunk = { size: 0, type: '', crc: 0, dataOffset: 0x00, endOffset: 0x00 };
    while (chunk.type !== 'IEND') {
        chunk = this.readChunk(chunk.endOffset);
        chunks.push(chunk);
    }
    return chunks;
}

IHDR 청크로 컬러 타입 알아내기

청크 배열을 얻었다면 이제 각각의 청크에 대한 상세 분석을 진행할 차례입니다. 대표 예로 IHDR 청크를 읽어서 컬러 타입을 읽어내는 방법을 살펴보겠습니다. 우선 IHDR 청크의 구조를 알아볼까요? IHDR 청크의 구조는 다음과 같습니다. 청크 필드에 컬러 모드를 알려주는 컬러 타입 필드가 있습니다. 이 컬러 타입을 읽어내는 방법을 함께 보겠습니다.

청크 필드 필드 크기(바이트)
가로 4
세로 4
비트 심도 1
컬러 타입 1
압축 방식 1
필터 방식 1
인터레이스 방식 1

다음의 코드를 이용하여 앞 섹션에서 얻어낸 청크 배열에서 IHDR 청크의 정보를 가지고 옵니다.

const ihdrChunk = chunks.find(function(chunk) {
    return chunk.type === 'IHDR'
})

얻어낸 IHDR 청크의 정보를 다음과 같이 한번에 읽어들입니다. 아래 코드에서 읽는 정보는 바로 위에서 함께 살펴본 IHDR 청크를 구성하는 필드들입니다. 컬러 타입의 사양은 PNG 사양 페이지의 Table 11.1을 참고해 주세요. 이로써 우리의 스티커 이미지가 RGB 컬러 타입인지 아닌지를 알 수 있게 되었습니다.

const offset = ihdrChunk.dataOffset;
return {
    width: dataView.getUint32(offset),
    height: dataView.getUint32(offset + 4),
    bitDepth: dataView.getUint8(offset + 8),
    colorType: dataView.getUint8(offset + 9),
    compression: dataView.getUint8(offset + 10),
    filter: dataView.getUint8(offset + 11),
    interlace: dataView.getUint8(offset + 12),
    crc: dataView.getUint32(offset + 13),
};

APNG로 확장하기

APNG는 일반적인 PNG에서 사용되는 청크 외에 'fcTL', 'fdAT', 'acTL' 같은 청크를 추가해서 PNG를 확장한 것입니다. 'acTL' 청크는 하나만 존재하며, 프레임 수나 루프(loop) 수 등의 정보가 'acTL' 청크 안에 포함됩니다. 'fcTL', 'fdAT' 청크는 여러 개 존재하며 각 프레임의 시간, 이미지 데이터, 표시 위치 등의 정보가 포함되어 있습니다. 이 글에서는 구체적인 설명을 생략하겠지만, 애니메이션 스티커 체커는 APNG 사양을 바탕으로 분석을 수행합니다. 자세한 APNG 사양에 대해서는 Mozilla의 Animated PNG graphics를 확인해 보세요.

일련 번호가 매겨진 이미지 재생하기

여담으로, 일련 번호가 매겨진 PNG 이미지를 재생할 때 <canvas> 요소를 이용하여 이미지를 표시하는 것도 한 방법인데요, 우리 체커 내에서는 간단하게 <img> 요소의 위치를 단계적으로 움직여서 애니메이션을 재생하고 있습니다. 참고로, 이미지의 투명도 설정이 제대로 되어 있는지 쉽게 알아볼 수 있도록 배경색이 들어갑니다.

ASC
스티커 이미지 보호를 위해 본 글에서는 스티커 그림이 보이지 않도록 처리했습니다.

맺으며

저는 일할 때 보통 Swift를 사용하기 때문에 Mac에서만 실행되는 도구를 만드는 것이 가장 쉽습니다. 크로스 플랫폼 도구를 만드는 방법으로 Electron도 있지만, 웹 브라우저에서 구동되는 도구를 만들면 Mac이나 Windows뿐 아니라 iOS나 Android 등에서도 동작하게 할 수 있습니다. 또한 버그 발생 등 여러 이유로 도구를 업데이트해야 할 때, 사용자로 하여금 새로운 파일을 다운로드하게 할 것 없이 서버 상의 파일만 업데이트하면 됩니다. HTML5 등장 이후 수행할 수 있는 기능이 많아져 웹 브라우저는 저에게 꽤 흥미로운 아이템이 되었습니다.


*일본에서는 스티커를 스탬프라고 부르기 때문에, 본 글에서 소개된 도구의 원래 이름은 '애니메이션 스탬프 체커'입니다.

AdventCalendar APNG

ha1f 2018.05.28

LINE Fukuoka에서 iOS 앱 개발을 담당하고 있습니다.

Add this entry to Hatena bookmark

리스트로 돌아가기