티스토리 뷰
https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing
CanvasRenderingContext2D#drawImage 메소드를 활용하면 클라이언트 수준에서의 사용자 입력 이미지 파일의 리사이즈 작업이 가능합니다만, 이미지 텍스처 내 경계선의 앨리어싱 문제가 발생하게 됩니다.
위 스택오버플로 링크의 답변에 의하면 이미지 다운스케일링 시 픽셀 보간을 2x2 샘플링(bi-linear)방식으로 하기 때문에 발생하는 문제로 추정된다고 하는데, 과연 나중의 해결책(다운스케일링을 원본 이미지 크기의 절반씩 반복하는 솔루션)의 결과물을 보면 이러한 추정이 합리적인 것 같습니다.
우선 캔버스를 통한 이미지 다운스케일링 알고리즘이 bi-linear 방식이라는 걸 어떻게 알 수 있을까요? 브라우저 렌더링 엔진의 소스를 열어보거나, 혹은 아래 링크의 어플리케이션을 통해서 알아볼 수 있을 것 같습니다. (이 포스트는 캔버스의 리사이징 이슈에 대한 것은 아니고 브라우저가 이미지를 다루는 방식에 대해 다루고 있습니다. 언급된 브라우저 버전들을 보니 너무 오래전 이야기이도 합니다.)
http://entropymine.com/resamplescope/notes/browsers/
아니 그 전에, 브라우저가 어떤 알고리즘을 사용하든 간에, 실제로 앨리어싱 이슈가 있느냐? 부터 알아보는 것이 더 좋겠습니다. 엔진 오버홀은 조금 나중에 해보도록 하지요. (정말 그렇게 알 수 있는 것이 맞습니까?)
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에서 확인하실 수 있습니다.
'Note' 카테고리의 다른 글
Jest, Jasmine의 matchers 메소드 비교, 대조 (0) | 2020.08.31 |
---|---|
캔버스를 활용한 이미지 다운스케일링 시 앨리어싱 문제 (0) | 2020.08.02 |
MongoDB - Enable Auth (0) | 2016.12.26 |
var, let, const (0) | 2016.07.09 |
PHP password_hash를 이용한 MySQL 비밀번호 저장 (0) | 2016.06.25 |