모두모두 개발자다요/javascript

캔버스 (canvas) 데이터를 이미지로(png) 변환 후 ajax 파일 전송(upload)

鬼미쿠 2019. 6. 4. 11:09

캔버스 화면에 그린 그림 데이터를 서버에 파일로 저장한다는 로직을 한번 풀어보기로 한다.

 

개발 환경 및 구현 로직 시나리오는

1. html5 canvas view 코딩

   : 그림판 기능 구현은 아니고 고정된 그림이 찍혀 있다고 가정

2. 그림 데이터를 이미지 파일화(?) 하여 java spring/mvc 웹 애플리케이션 서버로 업로드

   : jquery.ajax() 으로 submit() 구현

3. 서버 사이드 requestMapping 메소드에서 파일 파라미터 값 확인

   : 상세한 파일 생성 처리까지는 다루지 않고 Controller requestMapping 메소드에서 org.springframework.web.multipart.MultipartFile 객체 타입으로 전달되는지 확인하기로 한다.

 

html

<div>
    <canvas id="canvas" width="150" height="150"></canvas>
    <button type="button" onclick="saveImage();">save canvas to image</button>
</div>

 

javascript

$(document).ready(function () {
    draw();
});

// 빨간색 네모 하나 그려주는 function
function draw() {
    var canvas = document.getElementById('canvas');
    if (canvas.getContext) {
        var ctx = canvas.getContext('2d');
        ctx.fillStyle = 'rgb(200, 0, 0)';
        ctx.fillRect (10, 10, 50, 50);
    }
}

function saveImage() {
    // 여기에 2 번 로직을 구현한다.
}

 

2 번 로직 구현에서 고민한 부분

2-1. canvas 그림 값 데이터를 어떻게 얻느냐?

function saveImage() {
    var $canvas = document.createElement('canvas');
    var imgDataUrl = $canvas.toDataURL('image/png');
    ...
}

canvas 객체의 toDataURL() 메소드가 그림 데이터를 리턴한다.

"data:image/png;base64" 값으로 시작되는  base64 암호화된 데이터 값 (기이이인~~~ 값)

data:image/png;base64,암호화된이미지데이터값

콤마를 기준으로 앞 부분이 header 격의 정보라면 뒷 부분이 실제 이미지 데이터의 body 값이라고 보면 되겠다.

인자로 'image/png' 와 같이 이미지 type 값을 반영하여 리턴하는 듯

참고로 'image/png' 일 경우는 캔버스 영역의 (width, height) 빈 칸이 투명색으로,

'image/jpeg' 일 경우는 검정색 (rgb(0, 0, 0)) 으로 만들어진다.

 

2-2. 얻어온 그림 데이터 값을 어떻게 파일화 하느냐??

function saveImage() {
    var $canvas = document.createElement('canvas');
    var imgDataUrl = $canvas.toDataURL('image/png');
    
    var blobBin = atob(imgDataUrl.split(',')[1]);	// base64 데이터 디코딩
    var array = [];
    for (var i = 0; i < blobBin.length; i++) {
        array.push(blobBin.charCodeAt(i));
    }
    var file = new Blob([new Uint8Array(array)], {type: 'image/png'});	// Blob 생성
    var formdata = new FormData();	// formData 생성
    formdata.append("file", file);	// file data 추가
    ...
}

Blob 객체에 디코딩한 base64 데이터 값을 FormData() 에 담는 방식으로 구현한다.

참고로, atob() 메소드와 (반대는 btoa()) Uint8Array 객체는 모두 구식 브라우저 (IE9 같은 쓰레기) 에서의 완벽 구동을 보장하지 않는다. 관련한 pollyfill 기술을 가져다가 대체 적용은 가능하겠지만.. (검색은 해봤지만.. 건드려볼 생각은 없다.)

 

2-3. 근데 왜 자꾸 파일이 안 넘겨지냐?? 하던대로 그냥 ajax submit 하면 될줄 알았더니.. ??

function saveImage() {
    ...
    ...
    var formdata = new FormData();
    formdata.append("file", file);
    
    $.ajax({
        type : 'post',
        url : '/saveImage',
        data : formdata,
        processData : false,	// data 파라미터 강제 string 변환 방지!!
        contentType : false,	// application/x-www-form-urlencoded; 방지!!
        success : function (data) {
            ...
        }
        ...
    });
}

ajax 옵션 중에 추가로 'processData : false', 'contentType : false' 으로 설정을 해야 제대로 전송이 되는 것을 확인했다.

알아본 바, processData 옵션을 false 으로 명시하지 않으면 FormData 에 추가한 파일 데이터가 string 값으로 자동 변환되어 전송이 된다고 한다.

또 하나, 애초에 view 단에 <form enctype="mulipart-form/data"> 와 같이 전송할 데이터를 form element 으로부터 참조 받아온  데이터를 전송하는게 아니고. 직접 FormData 객체를 만들어 보내는 방식이라, ajax 으로 전송할 때 contentType 을 기본값인 'application/x-www-form-urlencoded;' 으로 인식하게 되는 것..;

(그래서 지금 해야할 과제 : contentType 을 'form-data' 으로 전송되게끔 하는 것!)

javascript 으로 동적으로 생성한 FormData 객체의 온전한(?) 전송을 위해 contentType : false 으로 설정하여 전송하면 의도한대로 파일 데이터 자체로(?) 서버에 전송되는 것을 확인할 수 있었다..

 

3. controller

@RequestMapping("/saveIamge")
public String saveIamge(@RequestParam(value="file", required=true) MultipartFile [] file) {
    ...
    logger.debug("file size : ", file[0].getSize());	// 서버로 무사히 안착됨
    ...
}