Skip to content

Instantly share code, notes, and snippets.

@sungchuni
Last active December 7, 2020 15:08
Show Gist options
  • Save sungchuni/45ef80c26f48084b98e50391c01048d2 to your computer and use it in GitHub Desktop.
Save sungchuni/45ef80c26f48084b98e50391c01048d2 to your computer and use it in GitHub Desktop.
polyfill();
const browser = (() => {
let cantRotate = null;
return {
get cantRotate() {
return new Promise(async (resolve, reject) => {
if (cantRotate === null) {
try {
const image = Object.assign(new Image(), {
src:
"data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAIAAwMBEQACEQEDEQH/xABRAAEAAAAAAAAAAAAAAAAAAAAKEAEBAQADAQEAAAAAAAAAAAAGBQQDCAkCBwEBAAAAAAAAAAAAAAAAAAAAABEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AG8T9NfSMEVMhQvoP3fFiRZ+MTHDifa/95OFSZU5OzRzxkyejv8ciEfhSceSXGjS8eSdLnZc2HDm4M3BxcXwH/9k=",
});
await image.decode();
const {width, height} = image;
resolve((cantRotate = !(width === 2 && height === 3)));
} catch (err) {
reject(err);
}
} else {
resolve(cantRotate);
}
});
},
};
})();
export default async function downscaleImage(file, option = {}) {
const {
mimeType = "image/jpeg",
quality = 0.8,
returnTypes = ["blob"],
square = false,
cropHorizontal = "center",
cropVertical = "top",
targetWidth = 1280,
} = option;
validation(file, {
mimeType,
quality,
returnTypes,
square,
cropHorizontal,
cropVertical,
targetWidth,
});
const image = await createImage(file);
const {canvas, ctx} = createCanvas(image, {
targetWidth,
square,
});
draw(image, canvas, ctx, {square, cropHorizontal, cropVertical});
return createResult(file, canvas, {
mimeType,
quality,
returnTypes,
});
}
function polyfill() {
if (!("decode" in HTMLImageElement.prototype)) {
Object.assign(HTMLImageElement.prototype, {
decode() {
return new Promise((resolve) => this.addEventListener("load", resolve));
},
});
}
if (
!("toBlob" in HTMLCanvasElement.prototype) &&
typeof HTMLCanvasElement.prototype.msToBlob === "function"
) {
Object.assign(HTMLCanvasElement.prototype, {
toBlob(resolve) {
resolve(this.msToBlob());
},
});
}
}
function validation(
file,
{
mimeType,
quality,
returnTypes,
square,
cropHorizontal,
cropVertical,
targetWidth,
}
) {
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 (!(typeof square === "boolean")) {
throw new TypeError("square should be boolean");
} else if (!["left", "center", "right"].includes(cropHorizontal)) {
throw new TypeError(
"crop align in horizontal should be one of left, center, right"
);
} else if (!["top", "center", "bottom"].includes(cropVertical)) {
throw new TypeError(
"crop align in vertical should be one of top, center, bottom"
);
} else if (!Number.isFinite(targetWidth)) {
throw new TypeError("target width should be finite number");
} else if (
!Array.isArray(returnTypes) ||
returnTypes.length === 0 ||
!returnTypes.every((returnType) => ["blob", "dataURL"].includes(returnType))
) {
throw new TypeError(
"return type in return types should be one of blob or dataURL"
);
}
}
async function createImage(file) {
const objectURL = window.URL.createObjectURL(file);
const image = Object.assign(new Image(), {
src: objectURL,
});
await image.decode();
window.URL.revokeObjectURL(objectURL);
return image;
}
function createCanvas(image, {targetWidth, square}) {
const ratio = image.height / image.width;
const width = square
? Math.min(targetWidth, image.width, image.height)
: Math.min(targetWidth, image.width);
const height = width * (square || ratio);
const canvas = Object.assign(document.createElement("canvas"), {
width,
height,
});
const ctx = canvas.getContext("2d");
return {canvas, ctx};
}
function draw(image, canvas, ctx, {square, cropHorizontal, cropVertical}) {
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const {width, height} = canvas;
if ("imageSmoothingQuality" in ctx) {
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = "high";
if (square) {
const size = Math.min(image.width, image.height);
const sx = {
left: 0,
center: (image.width - size) * 0.5,
right: image.width - size,
}[cropHorizontal];
const sy = {
top: 0,
center: (image.height - size) * 0.5,
bottom: image.height - size,
}[cropVertical];
ctx.drawImage(image, sx, sy, size, size, 0, 0, width, height);
} else {
ctx.drawImage(image, 0, 0, width, height);
}
} else {
const offscreenCanvas = Object.assign(document.createElement("canvas"), {
width: image.width,
height: image.height,
});
const offscreenCtx = offscreenCanvas.getContext("2d");
offscreenCtx.drawImage(image, 0, 0);
const total = Math.max(
Math.ceil(Math.log(image.width / width) / Math.log(2)) - 1,
0
);
const iteration = {total, count: total};
while (iteration.count-- > 0) {
offscreenCtx.drawImage(
offscreenCanvas,
0,
0,
offscreenCanvas.width * 0.5,
offscreenCanvas.height * 0.5
);
}
const scaledWidth = offscreenCanvas.width * 0.5 ** iteration.total;
const scaledHeight = offscreenCanvas.height * 0.5 ** iteration.total;
if (square) {
const size = Math.min(scaledWidth, scaledHeight);
ctx.drawImage(
offscreenCanvas,
(scaledWidth - size) * 0.5,
(scaledHeight - size) * 0.5,
size,
size,
0,
0,
width,
height
);
} else {
ctx.drawImage(
offscreenCanvas,
0,
0,
scaledWidth,
scaledHeight,
0,
0,
width,
height
);
}
}
}
async function createResult(file, canvas, {mimeType, quality, returnTypes}) {
const cantRotate = await browser.cantRotate;
const resultCanvas = cantRotate
? await getRotatedCanvas(file, canvas)
: canvas;
const result = {};
if (returnTypes.includes("blob")) {
result.blob = await new Promise((resolve) =>
resultCanvas.toBlob(resolve, mimeType, quality)
);
}
if (returnTypes.includes("dataURL")) {
result.dataURL = resultCanvas.toDataURL(mimeType, quality);
}
return result;
}
async function getRotatedCanvas(file, canvas) {
const {width, height} = canvas;
const {value} = await parseOrientation(file);
if ([3, 6, 8].includes(value)) {
const {PI} = Math;
const [angle, x, y] =
(value === 3 && [PI, -width, -height]) ||
(value === 6 && [PI * 0.5, 0, -height]) ||
(value === 8 && [-PI * 0.5, -width, 0]);
const rotatedCanvas = Object.assign(
document.createElement("canvas"),
angle % PI ? {width: height, height: width} : {width, height}
);
const rotatedCtx = rotatedCanvas.getContext("2d");
rotatedCtx.rotate(angle);
rotatedCtx.translate(x, y);
rotatedCtx.drawImage(canvas, 0, 0, width, height, 0, 0, width, height);
return rotatedCanvas;
} else {
return canvas;
}
}
async function parseOrientation(blob) {
const APP1_MARKER = 0xffe1;
const EXIF_HEADER = Array.from("Exif")
.map((c) => c.charCodeAt())
.reverse()
.reduce(
(accumulator, charCode, index) =>
accumulator + charCode * 16 ** (index * 2)
);
const INTEL_BYTE_ALIGN = 0x4949;
const ORIENTATION_TAG_NO = 0x0112;
const orientation = {
value: 9,
title: undefined,
};
const arrayBuffer = await getArrayBuffer(blob);
const dataView = {
exif: null,
whole: new DataView(arrayBuffer),
};
for (let i = 0; i < dataView.whole.byteLength - 4; i++) {
if (
dataView.whole.getUint16(i) === APP1_MARKER &&
dataView.whole.getUint32(i + 4) === EXIF_HEADER
) {
const size = dataView.whole.getUint16(i + 2);
dataView.exif = new DataView(arrayBuffer.slice(i, size));
break;
}
}
if (dataView.exif === null) {
return orientation;
}
const littleEndian = dataView.exif.getUint16(10) === INTEL_BYTE_ALIGN;
for (let i = 0; i < dataView.exif.byteLength - 2; i = i + 2) {
if (dataView.exif.getUint16(i, littleEndian) === ORIENTATION_TAG_NO) {
const value = dataView.exif.getUint16(i + 8, littleEndian);
const title =
(value === 1 && "upper left") ||
(value === 3 && "lower right") ||
(value === 6 && "upper right") ||
(value === 8 && "lower left") ||
undefined;
Object.assign(orientation, {value, title});
break;
}
}
return orientation;
}
function getArrayBuffer(blob) {
if ("arrayBuffer" in blob) {
return blob.arrayBuffer();
} else {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob);
fileReader.onloadend = ({target: {result}}) => resolve(result);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment