html5 Canvas를 이용해 png 이미지 파일 만들기

캔버스에 그린 이미지를 PNG 이미지로 저장하는 방법이 필요해 찾아봤다. 평소에 구현해본게 아니라서 약간의 삽질이 있었지만 파일 구조를 좀 알고 있다면 그다지 어려운 일은 아니다. 참고로 본 설명은 Windows8 스토어 앱 개발에 필요했던 작업이라 WinJS 기준으로 설명하겠지만, 기본 원리는 다른 플랫폼이라도 동일할 것으로 생각된다.

1. 캔버스(Canvas)에 여러장의 이미지 로드하기

먼저 canvas를 이용해 이미지를 올려보자. HTML에 <canvas> 태그를 삽입하고 다음과 같은 코드를 이용해 이미지를 로드해본다.

var canvas = document.getElementById('canvas');
var cx = canvas.getContext('2d');
var imageObj = new Image();
imageObj.onload = function () {
    cx.drawImage(imageObj, 69, 50);
}
imageObj.src = 'http://www.html5canvastutorials.com/demos/assets/darth-vader.jpg';

document.getElementById('save').onclick = function () {
    saveToFile('test.png');
}

document.getElementById('load').onclick = function () {
    loadFromFile("test.png");
}

2. 캔버스 이미지 데이터 뽑아내기

이제 저장하기 버튼을 눌러서 캔버스에 그린 녀석들을 PNG 파일로  저장해보자. 저장할때는 캔버스에 있는 .toDataURL(‘image/png’) 이라는 메소드를  사용한다. 단, 원격지에 있는 이미지를 로드한 경우 보안 문제로 인해 다음과 같은 에러 메시지를 출력할 것이다.

Error: SECURITY_ERR: DOM Exception 18

따라서 .toDataURL() 메소드를 사용하려면 캔버스에 로드된 파일이 반드시 로컬에 위치해 있어야한다. 여튼 이 메소드는 캔버스의 픽셀 이미지를 Base64로 인코딩해서 다음과 같은 형태의 문자열 데이터로 출력해준다.

"data:image/png;base64,iVBORw0KGgoAAAANSuhEUgAAArwAAAH0......"

참고로 이 값을 이미지 태그의 src 주소에 넣으면 바로 이미지가 로드된다. 하지만 파일로 저장하려면 약간의 트릭이 필요하다. 먼저 PNG 데이터를 끄집어 내야한다.

3. PNG 이미지 데이터로 Blob 데이터 파일 만들기

PNG 파일에는 실제 이미지 데이터가 저장되어야 하기때문에 toDataURL() 로 출력된 문자열에서 “data:image/png;base64,” 이후로 나열된 문자열을 다시 디코딩해서 저장한다. 이런 이미지 데이터를 블럽(Blob) 데이터라고 얘기한다. WinJS의 경우 다음과 같은 메소드를 이용해 디코딩할수있다.

Windows.Security.Cryptography.CryptographicBuffer.decodeFromBase64String(인코드데이터)

파일로 저장하기위한 전체 메소드는 다음과 같이 작성했다. localFolder 객체는 앱 안에 저장되는 위치다.

var saveToFile = function (path) {
    var canvas = document.getElementById('canvas'),
        data = canvas.toDataURL('image/png'),
        localFolder = Windows.Storage.ApplicationData.current.localFolder,
        encodeData = data.replace("data:image/png;base64,", ""),
        decode = Windows.Security.Cryptography.CryptographicBuffer.decodeFromBase64String(encodeData);

    // mySample.txt의 이름으로 파일을 생성하고 동일한 이름이 있을 경우, 덮어쓴다.
    localFolder.createFileAsync(path, Windows.Storage.CreationCollisionOption.replaceExisting)
        .then(function (file) {
            // writeTextAsync메소들 통해 파일에 텍스트를 쓴다.
            return Windows.Storage.FileIO.writeBufferAsync(file, decode);
        });        
    };

4. 이미지 파일 로드하기

서두에도 잠깐 언급했지만, 이미지 파일을 로드는 간단하다. 그냥 이미지 파일 패스를 이미지 태그의 src 주소로 넣으면 끝이다. 일단 localFolder에 저장했으므로 localFolder에서 이미지 패스를 구해서 넣으면 끝! 코드는 아래와 같다.

var loadFromImage = function (path) {
    var localFolder = Windows.Storage.ApplicationData.current.localFolder;

    localFolder.getFileAsync(path).done(function (file) {
        var elTest = document.getElementById('test'), 
            img = new Image();

        img.src = URL.createObjectURL(file);
        elTest.appendChild(img);
    });    
};

Blob 이미지를 SQLite에 저장하고 불러오기

Blob 객체란?

타이타늄 모바일에서 Blob 객체는 내부 데이터를 바이너리로 가지고 있는 객체를 의미한다.

The blob is an abstract data type that represents binary information, often obtained through HTTPClient or via files. It often is used to store text or the actual data of an image.

그리고 종종 이 객체를 그대로 데이터베이스에 저장하고 싶을때가 있다. 예를 들면 카메라로 찍은 이미지를 서버에 저장하지 않고, 그냥 앱안에 저장하고 싶은 경우가  있을수 있다. 이런경우엔, 보통 2가지를 생각해볼 수있다.

  1. SQLite 를 이용해 이미지를 앱 안에 저장한다.
  2. 찍은 이미지를 로컬 파일 시스템에 저장하고, Native Path를 DB에 저장한다.

여기서 다룰 내용은 바로 1번에 해당하는 내용이다. 그럼, 본론으로 들어가자!

Blob 이미지와 SQLite

타이타늄 모바일 커뮤니티 Q&A 게시판을 뒤져보면, Blob 객체를 SQLite DB에 저장하고 싶어하는 사람들이 많다는 사실을 알수있다. 하지만 그 어떤 누구도 뾰족한 해답을 제시해주지 못하고 있다.

SQLite에 쓰려면 직렬화 하세요!

문제의 핵심은 바로 SQLite DB에 있다. SQLite 가 지원하는 데이터 타입에 분명 BLOB 타입이 존재하지만, 자바스크립트에서 쓰는 그 Blob 객체가 그대로 저장된다는 의미는 아니다. 결국, DB에 데이터를 쓰려면 객체를 직렬화해야한다. 가장 간단한 방법은 객체를 문자열로 만들면 된다. 그럼, Ti.Blob 객체가 제공하는 toString() 메소드를 이용하면 되겠네? 라고 생각할수도 있지만, 결론은 안된다. 그럼 도대체 어쩌란 말인가???

바이너리 객체를 인코딩하세요!

결론은 base64로 인코딩했다가, 디코딩해서 쓰면된다.  코드로 써보면 다음과 같다.

// 저장할땐, 인코딩
exports.setImage = function(_rowId, _blob) {
    var mydb = Ti.Database.open(DATABASE_NAME);
    var sImg = Ti.Utils.base64encode(_blob);
    mydb.execute('UPDATE [table_name] SET blobImg="'+ sImg +'" WHERE  rowId="'+_rowId+'"');
    mydb.close();
};

// 사용할때, 디코딩
var db = require('db');
var encodedImg = db.getImage(rowId);
var blobImg = Ti.Utils.base64decode(encodedImg);

Summary

  1. save Blob image to SQLite

    var sImg = Ti.Utils.base64encode(_blob);

  2. use Blob image from SQLite

    var encodedImg = db.getImage(rowId); var blobImg = Ti.Utils.base64decode(encodedImg) ;

  3. but it is too slow in titanium, don’t use it!

덧,

몇가지 테스트해서 구현해봤더니 이미지를 DB에 때려 박는 무식한 방법은 쓰지 않는게 좋겠다. 이미지를 Base64로 인코딩하고 디코딩하는데 시간이 너무 오래 걸린다. 여튼 위 방법은 속도가 엄청 느리므로 비추한다.