티스토리 뷰
https://sungchuni.tistory.com/12
캔버스를 통한 이미지 다운스케일링 작업 시 발생하는 앨리어싱(계단 현상) 문제에 대해 임상적으로 알아보겠습니다.
우선 깨끗한 body를 준비하고, 스크립트 링크도 연결합니다.
<!-- /index.html --> <!DOCTYPE html> <html> <head> <script src="./index.js" defer></script> </head> <body></body> </html>
이미지 파일을 업로드할 파일 타입의 입력 상자와, 리사이즈 목표 픽셀을 입력할 숫자 타입의 입력 상자를 추가합니다.
// /index.js const inputElements = { file: null, number: null }; function createInputFile() { const input = document.createElement("input"); Object.assign(input, { accept: "image/*", multiple: true, type: "file" }); return document.body.appendChild(input); } function createInputNumber() { const input = document.createElement("input"); Object.assign(input, { min: 0, type: "number", value: 400 }); return document.body.appendChild(input); } function main() { inputElements.file = createInputFile(); inputElements.number = createInputNumber(); } window.addEventListener("DOMContentLoaded", main, {once: true});
파일 입력 시의 핸들러 함수를 정의합니다. 입력된 파일이 이미지 형식이면 이미지 요소와 objectURL을 만들어둡니다. 나중에 CanvasRenderingContext2D#drawImage 메소드를 호출할 때에 사용될 것입니다.
// /index.js // ... const objectURLs = new Set(); // ... function handleChange(event) { const {files} = event.target; for (const file of files) { if (!file.type.startsWith("image/")) continue; const image = new Image(); const objectURL = window.URL.createObjectURL(file); objectURLs.add(objectURL); image.src = objectURL; } } // ... function main() { // ... inputElements.file.addEventListener("change", handleChange); } // ...
이제 원본 이미지가 준비되었습니다. 캔버스 요소와 렌더링 컨텍스트를 반환하는 헬퍼 함수를 작성하고, 우선 기본적인 방식으로 drawImage 메소드를 사용해봅시다.
function createCanvas(image) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const ratio = image.height / image.width; const width = Number(inputElements.number.value) || image.width; const height = width * ratio; Object.assign(canvas, {width, height}); document.body.appendChild(canvas); return {canvas, ctx}; } function renderDefault(image) { const {canvas, ctx} = createCanvas(image); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); }
두번째 방식은 CanvasRenderingContext2D#imageSmoothingQuality 속성을 사용하는 방식입니다. 이것은 파이어폭스와 IE에서 지원되지 않습니다. (물론 지금 작성하는 자바스크립트 프로그램은 IE에서 실행되지 않습니다.)
function renderSmoothing(image, quality) { const {canvas, ctx} = createCanvas(image); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = quality; ctx.drawImage(image, 0, 0, canvas.width, canvas.height); }
마지막 방식은 원본 이미지의 절반씩 다운스케일링을 진행하는 방식입니다. 브라우저가 다운스케일링 시 2x2 샘플링을 사용하는 것으로 알려져 있어, 이와 같은 방식으로 안티앨리어싱을 적용할 수 있겠습니다. 반복 횟수만큼 최종 타겟 이미지의 픽셀 하나가 사용하게 되는 샘플 크기가 늘어납니다. (2번 다운스케일링을 진행한다면 4x4 샘플링, 3번이라면 8x8 샘플링 등등)
function renderSupersample(image) { const {canvas, ctx} = createCanvas(image); const offscreenCanvas = document.createElement("canvas"); const offscreenCtx = offscreenCanvas.getContext("2d"); Object.assign(offscreenCanvas, {width: image.width, height: image.height}); offscreenCtx.drawImage(image, 0, 0); const total = Math.max( Math.floor(Math.log(image.width / canvas.width) / Math.log(2)), 0 ); const iteration = {total, count: total}; while (iteration.count-- > 0) { offscreenCtx.drawImage( offscreenCanvas, 0, 0, offscreenCanvas.width * 0.5, offscreenCanvas.height * 0.5 ); } ctx.drawImage( offscreenCanvas, 0, 0, offscreenCanvas.width * 0.5 ** iteration.total, offscreenCanvas.height * 0.5 ** iteration.total, 0, 0, canvas.width, canvas.height ); }
이제 handleChange 함수에 위 함수들을 추가합니다. 파일이 다시 입력되면 기존의 objectURL과 캔버스 요소를 삭제하여, 새로운 결과물을 확인할 수 있도록 합니다.
async function handleChange(event) { objectURLs.forEach((objectURL) => { window.URL.revokeObjectURL(objectURL); objectURLs.delete(objectURL); }); document.querySelectorAll("canvas").forEach((canvas) => { document.body.removeChild(canvas); }); const {files} = event.target; for (const file of files) { if (!file.type.startsWith("image/")) continue; const image = new Image(); const objectURL = window.URL.createObjectURL(file); objectURLs.add(objectURL); image.src = objectURL; await image.decode(); renderDefault(image); ["low", "medium", "high"].forEach(renderSmoothing.bind(null, image)); renderSupersample(image); } }
렌더링한 결과물을 조금 더 쉽게 비교할 수 있도록 drawText, strokeText 메소드를 호출하는 헬퍼 함수를 하나 만듭니다.
function drawText(canvas, ctx, text) { ctx.font = "bold 32px sans-serif"; ctx.fillStyle = "white"; ctx.strokeStyle = "black"; ctx.textAlign = "center"; ctx.fillText(text, canvas.width * 0.5, canvas.height * 0.5); ctx.strokeText(text, canvas.width * 0.5, canvas.height * 0.5); } function renderDefault(image) { // ... drawText(canvas, ctx, "default"); } function renderSmoothing(image, quality) { // ... drawText(canvas, ctx, quality); } function renderSupersample(image) { // ... drawText(canvas, ctx, "super sample"); }
이것으로 됐습니다.
완성된 소스 코드는 https://github.com/sungchuni/canvas-aliasing에서,
프로그램은 https://sungchuni.github.io/canvas-aliasing/에서 보실 수 있습니다.
각자 이미지 파일을 입력해봅니다. 이미지 내용에 따라 그 정도는 다르겠지만 대부분의 경우 기본 설정의 렌더링 시 앨리어싱 문제가 있다는 것을 직접 확인할 수 있습니다.
'Note' 카테고리의 다른 글
단일 함수에 대한 브라우저 환경의 자바스크립트 테스트 작성 (0) | 2020.09.06 |
---|---|
Jest, Jasmine의 matchers 메소드 비교, 대조 (0) | 2020.08.31 |
캔버스를 활용한 이미지 다운스케일링 시 안티앨리어싱 적용 (0) | 2020.08.01 |
MongoDB - Enable Auth (0) | 2016.12.26 |
var, let, const (0) | 2016.07.09 |