grungnet/src/helpers/image.ts
Mitchell McCaffrey ecfab87aa0 More typescript
2021-07-09 16:22:35 +10:00

246 lines
6.1 KiB
TypeScript

import imageOutline from "image-outline";
import blobToBuffer from "./blobToBuffer";
import Vector2 from "./Vector2";
import { Outline } from "../types/Outline";
const lightnessDetectionOffset = 0.1;
/**
* @param {HTMLImageElement} image
* @returns {boolean} True is the image is light
*/
export function getImageLightness(image: HTMLImageElement): boolean {
const width = image.width;
const height = image.height;
let canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
let context = canvas.getContext("2d");
if (!context) {
return false;
}
context.drawImage(image, 0, 0);
const imageData = context.getImageData(0, 0, width, height);
const data = imageData.data;
let lightPixels = 0;
let darkPixels = 0;
// Loop over every pixels rgba values
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const max = Math.max(Math.max(r, g), b);
if (max < 128) {
darkPixels++;
} else {
lightPixels++;
}
}
const norm = (lightPixels - darkPixels) / (width * height);
return norm + lightnessDetectionOffset >= 0;
}
type CanvasImage = {
file: Uint8Array;
width: number;
height: number;
mime: string;
};
/**
* @param {HTMLCanvasElement} canvas
* @param {string} type
* @param {number} quality
* @returns {Promise<CanvasImage>}
*/
export async function canvasToImage(
canvas: HTMLCanvasElement,
type: string,
quality: number
): Promise<CanvasImage | undefined> {
return new Promise((resolve) => {
canvas.toBlob(
async (blob) => {
if (blob) {
const file = await blobToBuffer(blob);
resolve({
file,
width: canvas.width,
height: canvas.height,
mime: type,
});
} else {
resolve(undefined);
}
},
type,
quality
);
});
}
/**
* @param {HTMLImageElement} image the image to resize
* @param {number} size the size of the longest edge of the new image
* @param {string} type the mime type of the image
* @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<CanvasImage | undefined>}
*/
export async function resizeImage(
image: HTMLImageElement,
size: number,
type: string,
quality: number
): Promise<CanvasImage | undefined> {
const width = image.width;
const height = image.height;
const ratio = width / height;
let canvas = document.createElement("canvas");
if (ratio > 1) {
canvas.width = size;
canvas.height = Math.round(size / ratio);
} else {
canvas.width = Math.round(size * ratio);
canvas.height = size;
}
let context = canvas.getContext("2d");
if (context) {
context.drawImage(image, 0, 0, canvas.width, canvas.height);
} else {
return undefined;
}
return await canvasToImage(canvas, type, quality);
}
/**
* Create a image file with resolution `size`x`size` with cover cropping
* @param {HTMLImageElement} image the image to resize
* @param {string} type the mime type of the image
* @param {number} size the width and height of the thumbnail
* @param {number} quality if image is a jpeg or webp this is the quality setting
*/
export async function createThumbnail(
image: HTMLImageElement,
type: string,
size = 300,
quality = 0.5
): Promise<CanvasImage | undefined> {
let canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
let context = canvas.getContext("2d");
const ratio = image.width / image.height;
if (ratio > 1) {
const center = image.width / 2;
const halfHeight = image.height / 2;
if (context) {
context.drawImage(
image,
center - halfHeight,
0,
image.height,
image.height,
0,
0,
canvas.width,
canvas.height
);
}
} else {
const center = image.height / 2;
const halfWidth = image.width / 2;
if (context) {
context.drawImage(
image,
0,
center - halfWidth,
image.width,
image.width,
0,
0,
canvas.width,
canvas.height
);
}
}
return await canvasToImage(canvas, type, quality);
}
/**
* Get the outline of an image
* @param {HTMLImageElement} image
* @returns {Outline}
*/
export function getImageOutline(
image: HTMLImageElement,
maxPoints: number = 100
): Outline {
// Basic rect outline for fail conditions
const defaultOutline: Outline = {
type: "rect",
x: 0,
y: 0,
width: image.width,
height: image.height,
};
try {
let outlinePoints = imageOutline(image, {
opacityThreshold: 1, // Allow everything except full transparency
});
if (outlinePoints) {
if (outlinePoints.length > maxPoints) {
outlinePoints = Vector2.resample(outlinePoints, maxPoints);
}
const bounds = Vector2.getBoundingBox(outlinePoints);
// Reject outline if it's area is less than 5% of the image
const imageArea = image.width * image.height;
const area = bounds.width * bounds.height;
if (area < imageArea * 0.05) {
return defaultOutline;
}
// Detect if the outline is a rectangle or circle
if (Vector2.rectangular(outlinePoints)) {
return {
type: "rect",
x: Math.round(bounds.min.x),
y: Math.round(bounds.min.y),
width: Math.round(bounds.width),
height: Math.round(bounds.height),
};
} else if (
Vector2.circular(
outlinePoints,
Math.max(bounds.width / 10, bounds.height / 10)
)
) {
return {
type: "circle",
x: Math.round(bounds.center.x),
y: Math.round(bounds.center.y),
radius: Math.round(Math.min(bounds.width, bounds.height) / 2),
};
} else {
// Flatten and round outline to save on storage size
const points = outlinePoints
.map(({ x, y }) => [Math.round(x), Math.round(y)])
.flat();
return { type: "path", points };
}
} else {
return defaultOutline;
}
} catch {
return defaultOutline;
}
}