Skip to content

Instantly share code, notes, and snippets.

@uhop
Last active July 4, 2020 15:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save uhop/6b0dc8627981ea738264ab6fefb8151a to your computer and use it in GitHub Desktop.
Save uhop/6b0dc8627981ea738264ab6fefb8151a to your computer and use it in GitHub Desktop.
Image-related utilities for use in browsers.
let flag;
const detectWebp = () => {
if (typeof flag == 'boolean') return flag;
const canvas = document.createElement('canvas');
flag = !!(canvas.getContext && canvas.getContext('2d') && /^data:image\/webp;/.test(canvas.toDataURL('image/webp')));
return flag;
};
export default detectWebp;
// helper to copy rectangles
class Copier {
constructor(srcImageData, dstImageData) {
this.srcImageData = srcImageData;
this.dstImageData = dstImageData;
this.srcLineSize = this.srcImageData.width << 2;
this.dstLineSize = this.dstImageData.width << 2;
this.sw = this.srcImageData.width;
this.sh = this.srcImageData.height;
this.dw = this.dstImageData.width;
this.dh = this.dstImageData.height;
}
copy(sx, sy, dx, dy, w, h, step = 1) {
const srcOffset = sy * this.srcLineSize + (sx << 2),
dstOffset = dy * this.dstLineSize + (dx << 2),
count = w << 2,
src = this.srcImageData.data,
dst = this.dstImageData.data,
srcLineSize = step * this.srcLineSize,
dstLineSize = this.dstLineSize;
for (let j = 0, ps = srcOffset, pd = dstOffset; j < h; ++j, ps += srcLineSize, pd += dstLineSize) {
for (let i = 0; i < count; ++i) {
dst[pd + i] = src[ps + i];
}
}
}
}
export const extendImageData = (srcImageData, dstImageData, dx, dy, style) => {
if (dx < 0 || dy < 0) throw Error('Negative offsets');
if (srcImageData.width > dstImageData.width || srcImageData.height > dstImageData.height) throw Error('Source image is bigger than destination');
if (srcImageData.width + dx > dstImageData.width || srcImageData.height + dy > dstImageData.height)
throw Error('Source image with offset is outside of destination');
const copier = new Copier(srcImageData, dstImageData),
sw = copier.sw,
sh = copier.sh,
dw = copier.dw,
dh = copier.dh;
// copy the image
copier.copy(0, 0, dx, dy, sw, sh);
if (style === 'extend') {
// extend top and bottom
copier.copy(0, 0, dx, 0, sw, dy, 0);
copier.copy(0, sh - 1, dx, dy + sh, sw, dh - sh - dy, 0);
// extend left and right
for (let i = 0; i < dx; ++i) {
copier.copy(0, 0, i, dy, 1, sh);
copier.copy(sw - 1, 0, dx + sw + i, dy, 1, sh);
}
// extend corners
for (let i = 0; i < dx; ++i) {
copier.copy(0, 0, i, 0, 1, dy, 0); // TL
copier.copy(0, sh - 1, i, dy + sh, 1, dh - sh - dy, 0); // BL
copier.copy(sw - 1, 0, dx + sw + i, 0, 1, dy, 0); // TR
copier.copy(sw - 1, sh - 1, dx + sw + i, dy + sh, 1, dh - sh - dy, 0); // TL
}
return dstImageData;
}
if (
dx > srcImageData.width ||
dy > srcImageData.height ||
dstImageData.width - srcImageData.width - dx > srcImageData.width ||
dstImageData.height - srcImageData.height - dy > srcImageData.height
)
throw Error('Extension margins are bigger than the source image');
if (style === 'wrap') {
// extend top and bottom
copier.copy(0, sh - dy, dx, 0, sw, dy);
copier.copy(0, 0, dx, dy + sh, sw, dh - sh - dy);
// extend left and right
copier.copy(sw - dx, 0, 0, dy, dx, sh);
copier.copy(0, 0, dx + sw, dy, dw - sw - dx, sh);
// extend corners
copier.copy(sw - dx, sh - dy, 0, 0, dx, dy); // TL
copier.copy(0, 0, dx + sw, dy + sh, dw - sw - dx, dh - sh, dy); // BR
copier.copy(0, sh - dy, dx + sw, 0, dw - sw - dx, dy); // TR
copier.copy(sw - dx, 0, 0, dy + sh, dx, dh - sh - dy); // BL
return dstImageData;
}
if (style === 'mirror') {
// extend top and bottom
copier.copy(0, dy - 1, dx, 0, sw, dy, -1);
copier.copy(0, sh - 1, dx, dy + sh, sw, dh - sh - dy, -1);
// extend left and right
for (let i = 0; i < dx; ++i) {
copier.copy(i, 0, dx - i - 1, dy, 1, sh);
copier.copy(sw - i - 1, 0, dx + sw + i, dy, 1, sh);
}
// extend corners
for (let i = 0; i < dx; ++i) {
copier.copy(i, dy - 1, dx - i - 1, 0, 1, dy, -1); // TL
copier.copy(i, sh - 1, dx - i - 1, dy + sh, 1, dh - sh - dy, -1); // BL
copier.copy(sw - i - 1, dy - 1, dx + sw + i, 0, 1, dy, -1); // TR
copier.copy(sw - i - 1, sh - 1, dx + sw + i, dy + sh, 1, dh - sh - dy, -1); // TL
}
return dstImageData;
}
return null;
};
export const normalizeKernel = (m, weight) => {
const mh = m.length,
mw = m[0].length;
if (typeof weight != 'number' || isNaN(weight)) {
weight = 1 / (mh * mw);
}
for (let j = 0; j < mh; ++j) {
for (let i = 0; i < mw; ++i) {
m[j][i] *= weight;
}
}
return m;
};
// kernels
// taken from: https://en.wikipedia.org/wiki/Kernel_(image_processing)
export const edgeA = [
[1, 0, -1],
[0, 0, 0],
[-1, 0, 1]
];
edgeA.cx = edgeA.cy = 1;
export const edgeB = [
[0, -1, 0],
[-1, 4, -1],
[0, -1, 0]
];
edgeB.cx = edgeB.cy = 1;
export const edgeC = [
[-1, -1, -1],
[-1, 8, -1],
[-1, -1, -1]
];
edgeC.cx = edgeC.cy = 1;
export const sharpen = [
[0, -1, 0],
[-1, 5, -1],
[0, -1, 0]
];
sharpen.cx = sharpen.cy = 1;
export const boxBlur = normalizeKernel([
[1, 1, 1],
[1, 1, 1],
[1, 1, 1]
]);
boxBlur.cx = boxBlur.cy = 1;
export const gaussianBlur3 = normalizeKernel(
[
[1, 2, 1],
[2, 4, 2],
[1, 2, 1]
],
1 / 16
);
gaussianBlur3.cx = gaussianBlur3.cy = 1;
export const gaussianBlur5 = normalizeKernel(
[
[1, 4, 6, 4, 1],
[4, 16, 24, 16, 4],
[6, 24, 36, 24, 6],
[4, 16, 24, 16, 4],
[1, 4, 6, 4, 1]
],
1 / 256
);
gaussianBlur5.cx = gaussianBlur5.cy = 2;
export const unsharp5 = normalizeKernel(
[
[1, 4, 6, 4, 1],
[4, 16, 24, 16, 4],
[6, 24, -476, 24, 6],
[4, 16, 24, 16, 4],
[1, 4, 6, 4, 1]
],
-1 / 256
);
unsharp5.cx = unsharp5.cy = 2;
export const emboss = [
[-2, -1, 0],
[-1, 1, 1],
[0, 1, 2]
];
emboss.cx = emboss.cy = 1;
// main applicator
export const applyFilter = (srcImageData, dstImageData, m, cx, cy) => {
const mh = m.length,
mw = m[0].length,
w = srcImageData.width,
h = srcImageData.height,
lw = w - mw,
lh = h - mh,
shift = (cy * w + cx) << 2,
src = srcImageData.data,
dst = dstImageData.data;
for (let j = 0; j < lh; ++j) {
for (let i = 0; i < lw; ++i) {
const p = (j * w + i) << 2;
for (let c = 0; c < 4; ++c) {
let acc = 0;
for (let mj = 0, py = p; mj < mh; ++mj, py += w << 2) {
for (let mi = 0, px = py; mi < mw; ++mi, px += 4) {
acc += m[mj][mi] * src[px + c];
}
}
dst[p + shift + c] = Math.max(0, Math.min(255, Math.round(acc)));
}
}
}
return dstImageData;
};
const assert = require('assert').strict;
export const createHistogram = () => {
let hist = [0, 0, 0, 0];
hist = hist.concat(hist); // 8
hist = hist.concat(hist); // 16
hist = hist.concat(hist); // 32
hist = hist.concat(hist); // 64
hist = hist.concat(hist); // 128
hist = hist.concat(hist); // 256
return hist;
};
export const getHistogram = imageData => {
const hist = createHistogram(),
data = imageData.data;
for (let i = (imageData.width * imageData.height - 1) << 2; i >= 0; i -= 4) {
++hist[Math.max(0, Math.min(255, Math.round(0.2125 * data[i] + 0.7154 * data[i + 1] + 0.0721 * data[i + 2])))];
}
return hist;
};
export const getHistogramHsl = imageData => {
const hist = createHistogram(),
data = imageData.data;
for (let i = (imageData.width * imageData.height - 1) << 2; i >= 0; i -= 4) {
const r = data[i],
g = data[i + 1],
b = data[i + 2],
l = Math.round((Math.min(r, g, b) + Math.max(r, g, b)) / 2);
++hist[l];
}
return hist;
};
export const createLut = (a, b) => {
const lut = [];
for (let i = 0; i < 256; ++i) {
lut.push(Math.max(0, Math.min(Math.round(a * i + b))));
}
return lut;
};
export const combineLuts = (...luts) => {
const lut = createHistogram();
for (let i = 0; i < 256; ++i) {
let value = i;
for (let j = 0; j < luts.length; ++j) {
value = luts[j][value];
}
lut[i] = value;
}
return lut;
};
export const applyLut = (imageData, lut) => {
const data = imageData.data;
for (let i = (imageData.width * imageData.height - 1) << 2; i >= 0; i -= 4) {
data[i] = lut[data[i]];
data[i + 1] = lut[data[i + 1]];
data[i + 2] = lut[data[i + 2]];
}
return imageData;
};
export const applyLutHsl = (imageData, lut) => {
const data = imageData.data;
for (let i = (imageData.width * imageData.height - 1) << 2; i >= 0; i -= 4) {
// get RGB, convert to HSL
const r = data[i] / 255,
g = data[i + 1] / 255,
b = data[i + 2] / 255,
min = Math.min(r, g, b),
max = Math.max(r, g, b),
diff = max - min,
l = (min + max) / 2,
s = min == max ? 0 : l < 0.5 ? diff / (max + min) : diff / (2 - max - min);
let h = 0;
if (s) {
if (r === max) {
h = (g - b) / diff;
} else if (g === max) {
h = 2 + (b - r) / diff;
} else if (b === max) {
h = 4 + (r - g) / diff;
}
h *= 60;
if (h < 0) h += 360;
}
// applying LUT
const newL = lut[Math.round(l * 255)] / 255;
// convert new HSL back to RGB
if (!s) {
data[i] = data[i + 1] = data[i + 2] = Math.round(newL * 255);
continue;
}
const t1 = newL < 0.5 ? newL * (1 + s) : newL + s - newL * s,
t2 = 2 * newL - t1,
hue = h / 360,
channels = [hue + 0.333, hue, hue - 0.333];
for (let j = 0; j < 3; ++j) {
let value = channels[j];
value = value < 0 ? value + 1 : value > 1 ? value - 1 : value;
if (6 * value < 1) {
data[i + j] = Math.round((t2 + (t1 - t2) * 6 * value) * 255);
} else if (2 * value < 1) {
data[i + j] = Math.round(t1 * 255);
} else if (3 * value < 2) {
data[i + j] = Math.round((t2 + (t1 - t2) * 6 * (0.666 - value)) * 255);
} else {
data[i + j] = Math.round(t2 * 255);
}
}
}
return imageData;
};
export const limitHist = (hist, below = 0.03, above = 0.03) => {
// collect total
let total = 0;
for (let i = 0; i < 256; ++i) {
total += hist[i];
}
// find the lower bound
let sum = 0,
l = 0;
for (;;) {
const newSum = sum + hist[l];
if (newSum / total > below) break;
sum = newSum;
++l;
}
// find the upper bound
sum = 0;
let r = 255;
for (;;) {
const newSum = sum + hist[r];
if (newSum / total > above) break;
sum = newSum;
--r;
}
if (l >= r) return null;
const lut = [],
a = 255 / (r - l);
for (let i = 0; i < l; ++i) lut.push(0);
for (let i = l; i < r; ++i) lut.push(Math.round(a * (i - l)));
for (let i = r; i < 256; ++i) lut.push(255);
return lut;
};
export const equalizeHist = hist => {
// build cumulative distribution function
const cdf = [];
let runningTotal = 0;
for (let i = 0; i < 256; ++i) {
if (hist[i]) {
runningTotal += hist[i];
cdf.push(i, runningTotal);
}
}
if (!cdf.length) return null;
// build lut
const lut = [], minValue = cdf[1], span = runningTotal - minValue;
let lastIndex = -1, lastValue = 0;
for (let j = 0; j < cdf.length; j += 2) {
const index = cdf[j], value = (cdf[j + 1] - minValue) / span;
if (lastIndex + 1 < index) {
const a = (value - lastValue) / (index - lastIndex);
for (let i = lastIndex + 1; i < index; ++i) {
lut.push(Math.round(((i - lastIndex) * a + lastValue) * 255));
}
}
lut.push(Math.round(value * 255));
lastIndex = index;
lastValue = value;
}
for (let i = lastIndex + 1; i < 256; ++i) {
lut.push(255);
}
return lut;
};
export const getDataUrl = async file =>
new Promise(resolve => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.readAsDataURL(file);
});
export const getImage = async url =>
new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = e => reject(e);
image.src = url;
});
export const getDataUrlFromImage = (image, ...args) => {
const canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
return canvas.toDataURL(...args);
};
export const generateThumbnail = (image, width, height, filter, ...args) => {
const ratio = Math.min(width / image.width, height / image.height);
if (ratio >= 1) return getDataUrlFromImage(image, ...args);
const newWidth = Math.min(width, Math.ceil(image.width * ratio)),
newHeight = Math.min(height, Math.ceil(image.height * ratio));
const canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = newWidth;
canvas.height = newHeight;
ctx.clearRect(0, 0, newWidth, newHeight);
ctx.drawImage(image, 0, 0, newWidth, newHeight);
if (typeof filter == 'function') {
filter(canvas, ctx);
}
return canvas.toDataURL(...args);
};
import {getDataUrl, getImage, generateThumbnail} from './images';
import {unsharp5, applyFilter} from './imageFilters';
import detectWebp from './detectWebp';
const canUseWebp = detectWebp(),
parseDataUrl = /^data:([^;]+);/;
const makePreview = async (file, args = [], width = 128, height = 64) => {
if (!file || !/^image\//.test(file.type)) return null;
const dataUrl = await getDataUrl(file),
image = await getImage(dataUrl),
thumbUrls = {};
thumbUrls['image/png'] = generateThumbnail(image, width, height, (canvas, ctx) => {
const src = ctx.getImageData(0, 0, canvas.width, canvas.height),
dst = ctx.createImageData(canvas.width, canvas.height),
filter = unsharp5;
applyFilter(src, dst, filter, filter.cx, filter.cy);
ctx.putImageData(
dst,
filter.cx,
filter.cy,
filter.cx,
filter.cy,
canvas.width - filter[0].length + filter.cx + 1,
canvas.height - filter.length + filter.cy + 1
);
for (let i = 0; i < args.length; ++i) {
let type = args[i];
if (typeof type == 'string') type = [type];
if (!type[0] || (!canUseWebp && type[0] === 'image/webp')) continue;
try {
const url = canvas.toDataURL(...type);
if (!url) continue;
const parsed = parseDataUrl.exec(url);
if (!parsed || parsed[1] !== type[0]) continue;
thumbUrls[type[0]] = url;
} catch (e) {
// squelch
}
}
});
return thumbUrls;
};
export default makePreview;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment