@yireen/squoosh-browser
Version:
An image compression tool run in browser while @squoosh/lib can not.
171 lines (170 loc) • 5.61 kB
JavaScript
import * as WebCodecs from '../util/web-codecs';
import { drawableToImageData } from './canvas';
export function shallowEqual(one, two) {
for (const i in one)
if (one[i] !== two[i])
return false;
for (const i in two)
if (!(i in one))
return false;
return true;
}
async function decodeImage(url) {
const img = new Image();
img.decoding = 'async';
img.src = url;
const loaded = new Promise((resolve, reject) => {
img.onload = () => resolve();
img.onerror = () => reject(Error('Image loading error'));
});
if (img.decode) {
await img.decode().catch(() => null);
}
await loaded;
return img;
}
const canDecodeCache = new Map();
export function canDecodeImageType(type) {
if (!canDecodeCache.has(type)) {
const resultPromise = (async () => {
const picture = document.createElement('picture');
const img = document.createElement('img');
const source = document.createElement('source');
source.srcset = 'data:,x';
source.type = type;
picture.append(source, img);
await 0;
return !!img.currentSrc;
})();
canDecodeCache.set(type, resultPromise);
}
return canDecodeCache.get(type);
}
export function blobToArrayBuffer(blob) {
return new Response(blob).arrayBuffer();
}
export function blobToText(blob) {
return new Response(blob).text();
}
const magicNumberMapInput = [
[/^%PDF-/, 'application/pdf'],
[/^GIF87a/, 'image/gif'],
[/^GIF89a/, 'image/gif'],
[/^\x89PNG\x0D\x0A\x1A\x0A/, 'image/png'],
[/^\xFF\xD8\xFF/, 'image/jpeg'],
[/^BM/, 'image/bmp'],
[/^I I/, 'image/tiff'],
[/^II*/, 'image/tiff'],
[/^MM\x00*/, 'image/tiff'],
[/^RIFF....WEBPVP8[LX ]/s, 'image/webp'],
[/^\xF4\xFF\x6F/, 'image/webp2'],
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
[/^\xff\x0a/, 'image/jxl'],
[/^\x00\x00\x00\x0cJXL \x0d\x0a\x87\x0a/, 'image/jxl'],
];
const magicNumberToMimeType = new Map(magicNumberMapInput);
export async function sniffMimeType(blob) {
const firstChunk = await blobToArrayBuffer(blob.slice(0, 16));
const firstChunkString = Array.from(new Uint8Array(firstChunk))
.map((v) => String.fromCodePoint(v))
.join('');
for (const [detector, mimeType] of magicNumberToMimeType) {
if (detector.test(firstChunkString)) {
return mimeType;
}
}
return '';
}
export async function blobToImg(blob) {
const url = URL.createObjectURL(blob);
try {
return await decodeImage(url);
}
finally {
URL.revokeObjectURL(url);
}
}
export async function builtinDecode(blob, mimeType) {
if (await WebCodecs.isTypeSupported(mimeType)) {
try {
return await WebCodecs.decode(blob, mimeType);
}
catch (e) { }
}
const drawable = 'createImageBitmap' in self ? await createImageBitmap(blob) : await blobToImg(blob);
return drawableToImageData(drawable);
}
export function inputFieldValueAsNumber(field, defaultVal = 0) {
if (!field)
return defaultVal;
return Number(inputFieldValue(field));
}
export function inputFieldCheckedAsNumber(field, defaultVal = 0) {
if (!field)
return defaultVal;
return Number(inputFieldChecked(field));
}
export function inputFieldChecked(field, defaultVal = false) {
if (!field)
return defaultVal;
return field.checked;
}
export function inputFieldValue(field, defaultVal = '') {
if (!field)
return defaultVal;
return field.value;
}
export function konami() {
return new Promise((resolve) => {
const expectedPattern = '38384040373937396665';
let rollingPattern = '';
const listener = (event) => {
rollingPattern += event.keyCode;
rollingPattern = rollingPattern.slice(-expectedPattern.length);
if (rollingPattern === expectedPattern) {
window.removeEventListener('keydown', listener);
resolve();
}
};
window.addEventListener('keydown', listener);
});
}
export async function transitionHeight(el, opts) {
const { from = el.getBoundingClientRect().height, to = el.getBoundingClientRect().height, duration = 1000, easing = 'ease-in-out', } = opts;
if (from === to || duration === 0) {
el.style.height = to + 'px';
return;
}
el.style.height = from + 'px';
getComputedStyle(el).transform;
el.style.transition = `height ${duration}ms ${easing}`;
el.style.height = to + 'px';
return new Promise((resolve) => {
const listener = (event) => {
if (event.target !== el)
return;
el.style.transition = '';
el.removeEventListener('transitionend', listener);
el.removeEventListener('transitioncancel', listener);
resolve();
};
el.addEventListener('transitionend', listener);
el.addEventListener('transitioncancel', listener);
});
}
export function preventDefault(event) {
event.preventDefault();
}
export function assertSignal(signal) {
if (signal.aborted)
throw new DOMException('AbortError', 'AbortError');
}
export async function abortable(signal, promise) {
assertSignal(signal);
return Promise.race([
promise,
new Promise((_, reject) => {
signal.addEventListener('abort', () => reject(new DOMException('AbortError', 'AbortError')));
}),
]);
}