티스토리 뷰

https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing

 

Html5 canvas drawImage: how to apply antialiasing

Please have a look at the following example: http://jsfiddle.net/MLGr4/47/ var canvas = document.getElementById("canvas"); var ctx = canvas.getContext("2d"); img = new Image(); img.onload = func...

stackoverflow.com

CanvasRenderingContext2D#drawImage 메소드를 활용하면 클라이언트 수준에서의 사용자 입력 이미지 파일의 리사이즈 작업이 가능합니다만, 이미지 텍스처 내 경계선의 앨리어싱 문제가 발생하게 됩니다.


위 스택오버플로 링크의 답변에 의하면 이미지 다운스케일링 시 픽셀 보간을 2x2 샘플링(bi-linear)방식으로 하기 때문에 발생하는 문제로 추정된다고 하는데, 과연 나중의 해결책(다운스케일링을 원본 이미지 크기의 절반씩 반복하는 솔루션)의 결과물을 보면 이러한 추정이 합리적인 것 같습니다.

 

우선 캔버스를 통한 이미지 다운스케일링 알고리즘이 bi-linear 방식이라는 걸 어떻게 알 수 있을까요? 브라우저 렌더링 엔진의 소스를 열어보거나, 혹은 아래 링크의 어플리케이션을 통해서 알아볼 수 있을 것 같습니다. (이 포스트는 캔버스의 리사이징 이슈에 대한 것은 아니고 브라우저가 이미지를 다루는 방식에 대해 다루고 있습니다. 언급된 브라우저 버전들을 보니 너무 오래전 이야기이도 합니다.)

http://entropymine.com/resamplescope/notes/browsers/

 

How web browsers resize images

How web browsers resize images This is an analysis of the image resizing algorithms used by popular web browsers. It was made with the help of my ResampleScope utility. These tests were done years ago, and new versions of these browsers quite possibly work

entropymine.com

아니 그 전에, 브라우저가 어떤 알고리즘을 사용하든 간에, 실제로 앨리어싱 이슈가 있느냐? 부터 알아보는 것이 더 좋겠습니다. 엔진 오버홀은 조금 나중에 해보도록 하지요. (정말 그렇게 알 수 있는 것이 맞습니까?)

https://sungchuni.tistory.com/13

 

문제가 있다는 것을 잘 확인할 수 있군요. 그러면 빠르게 통합 함수를 작성하도록 해봅시다.

 

목표는, 렌더링 컨텍스트의 imageSmoothingQuality 속성을 사용할 수 있는 경우에는 브라우저 렌더링 엔진의 기능을 최대한 활용하고, 그렇지 않은 경우에만 (위 포스트에서 작성했던) renderSupersample 함수를 활용하는 쪽으로 프로그램을 작성하는 것입니다.

 

(맨 처음에 언급했던 스택오버플로의 답변에는 CanvasRenderingContext2D#fliter를 사용하는 방식에 대해서도 언급하고 있지만 아무래도 더 호환성이 좋지 않은 것 같으니 버리도록 합니다.)

 

downscaleImage 이름의, File 타입 file 매개변수를 받는 비동기 함수 선언문을 작성합니다. mime 형식이 image/* 인것도 확인합니다. 추출할 이미지의 크기도 받을 것인데, 우선 선택 항목optional으로 두고 적당한 임의의 기본값을 입력합니다.

async function downscaleImage(file, {targetWidth = 1280} = {}) {
  if (!(file instanceof File)) {
    throw new TypeError("file should be instance of File");
  } else if (!file.type.startsWith("image/")) {
    throw new TypeError("file type sholud be starts with image/");
  } else if (!Number.isFinite(targetWidth)) {
    throw new TypeError("target width should be finite number");
  }
}

계속해서 입력된 파일로 이미지 요소를 만들고, 디코딩을 기다린 후 이미지 크기를 측정, 캔버스 요소를 생성합니다.

// function downscaleImage
const image = new Image();
const objectURL = window.URL.createObjectURL(file);
image.src = objectURL;
await image.decode();
const ratio = image.height / image.width;
const width = Math.min(targetWidth, image.width);
const height = width * ratio;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
Object.assign(canvas, {width, height});

imageSmoothingQuality 속성이 지원되지 않는 브라우저에서 CanvasRenderingContext2D#imageSmoothingQuality를 호출하니 undefined를 반환합니다. 그러므로 우선 imageSmoothingQuality를 지원하는 브라우저를 위한 코드를 작성합니다.

// function downscaleImage
if (ctx.imageSmoothingQuality !== undefined) {
  ctx.imageSmoothingEnabled = true;
  ctx.imageSmoothingQuality = "high";
  ctx.drawImage(image, 0, 0, width, height);
}

else 블럭에는 renderSupersample 함수를 적용합니다. 맨 처음에 언급했던 스택오버플로의 답변을 참조하였습니다.

// function downscaleImage
else {
  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
    );
  }
  const offscreenWidth = offscreenCanvas.width * 0.5 ** iteration.total;
  const offscreenHeight = offscreenCanvas.height * 0.5 ** iteration.total;
  ctx.drawImage(
    offscreenCanvas,
    0,
    0,
    offscreenWidth,
    offscreenHeight,
    0,
    0,
    width,
    height
  );
}

샘플링 반복 횟수는 원본 너비 대 목표 너비의 비에 밑수base 2의 로그의 내림을 적용하여 계산합니다. 마지막 샘플링은 오프스크린 캔버스에서 캔버스로 가져오는 절차이기 때문에, 올림을 적용하지 않고 내림을 적용하였습니다. 목표 너비가 원본 너비가 큰 경우(image.width / canvas.width가 1보다 작을 경우)가 있을 수도 있으므로, 반복 횟수가 0 미만이 되지 않도록 합니다.

 

이제 다 되었습니다.

 

원본 이미지 파일은 다 사용하였으므로 revoke,

// function downscaleImage
window.URL.revokeObjectURL(objectURL);

선택 매개변수를 추가, 사용처에서 반환 형식을 결정하도록 하여,

// function downscaleImage
async function downscaleImage(
  file,
  {
    mimeType = "image/jpeg",
    quality = 0.8,
    returnType = "blob",
    targetWidth = 1280
  } = {}
) {
  if (!(file instanceof File)) {
    throw new TypeError("file should be instance of File");
  } else if (!file.type.startsWith("image/")) {
    throw new TypeError("file type sholud be starts with image/");
  } else if (!mimeType.startsWith("image/")) {
    throw new TypeError("mime type sholud be starts with image/");
  } else if (!Number.isFinite(quality) || quality < 0 || quality > 1) {
    throw new TypeError("quality should be finite number between 0 and 1");
  } else if (!Number.isFinite(targetWidth)) {
    throw new TypeError("target width should be finite number");
  } else if (!["blob", "dataURL"].includes(returnType)) {
    throw new TypeError("return type should be one of blob or dataURL");
  }
  // ...
}

최종 추출 부분을 작성합니다. IE를 위한 간단한 toBlob 폴리필 구문도 포함합니다. (core-js에 포함되지 않습니다.)

// function downscaleImage
if (returnType === "blob") {
  if (canvas.toBlob === undefined && typeof canvas.msToBlob === "function") {
    canvas.toBlob = resolve => resolve(canvas.msToBlob());
  }
  return new Promise(resolve => canvas.toBlob(resolve, mimeType, quality));
} else if (returnType === "dataURL") {
  return canvas.toDataURL(mimeType, quality);
}

이 기분대로 HTMLImageElement#decode 메소드도 폴리필을 작성합니다.

if (image.decode === undefined) {
  image.decode = () => new Promise(resolve => image.addEventListener("load", resolve))
}

완성된 함수는 https://gist.github.com/sungchuni/45ef80c26f48084b98e50391c01048d2에서 확인하실 수 있습니다.

 

단일 함수에 대한 브라우저 환경의 자바스크립트 테스트 작성

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크