122 lines
4.0 KiB
JavaScript
122 lines
4.0 KiB
JavaScript
'use strict';
|
|
|
|
(function attachBackgroundImage(globalScope) {
|
|
const MAX_BACKGROUND_EDGE = 1920;
|
|
const MAX_BACKGROUND_BYTES = 900 * 1024;
|
|
const BACKGROUND_IMAGE_TYPE = 'image/webp';
|
|
const BACKGROUND_IMAGE_QUALITIES = [0.82, 0.74, 0.66, 0.58, 0.5];
|
|
const BACKGROUND_IMAGE_EDGE_STEPS = [1, 0.88, 0.76, 0.64];
|
|
|
|
function constrainImageDimensions({ width, height, maxEdge = MAX_BACKGROUND_EDGE }) {
|
|
const safeWidth = Math.max(1, Math.round(Number(width) || 0));
|
|
const safeHeight = Math.max(1, Math.round(Number(height) || 0));
|
|
const safeMaxEdge = Math.max(1, Math.round(Number(maxEdge) || MAX_BACKGROUND_EDGE));
|
|
const largestEdge = Math.max(safeWidth, safeHeight);
|
|
|
|
if (largestEdge <= safeMaxEdge) {
|
|
return { width: safeWidth, height: safeHeight };
|
|
}
|
|
|
|
const scale = safeMaxEdge / largestEdge;
|
|
return {
|
|
width: Math.max(1, Math.round(safeWidth * scale)),
|
|
height: Math.max(1, Math.round(safeHeight * scale)),
|
|
};
|
|
}
|
|
|
|
function estimateDataUrlBytes(dataUrl) {
|
|
const value = String(dataUrl || '');
|
|
const marker = 'base64,';
|
|
const markerIndex = value.indexOf(marker);
|
|
if (markerIndex === -1) return 0;
|
|
|
|
const payload = value.slice(markerIndex + marker.length);
|
|
const padding = payload.endsWith('==') ? 2 : payload.endsWith('=') ? 1 : 0;
|
|
return Math.max(0, Math.floor((payload.length * 3) / 4) - padding);
|
|
}
|
|
|
|
function loadImageFromFile(file) {
|
|
return new Promise((resolve, reject) => {
|
|
const objectUrl = URL.createObjectURL(file);
|
|
const image = new Image();
|
|
|
|
image.onload = () => {
|
|
URL.revokeObjectURL(objectUrl);
|
|
resolve(image);
|
|
};
|
|
image.onerror = () => {
|
|
URL.revokeObjectURL(objectUrl);
|
|
reject(new Error('Could not read image'));
|
|
};
|
|
image.src = objectUrl;
|
|
});
|
|
}
|
|
|
|
async function compressImageFileForStorage(file, options = {}) {
|
|
if (!file || !String(file.type || '').startsWith('image/')) {
|
|
throw new Error('Please choose an image file');
|
|
}
|
|
|
|
const maxBytes = Math.max(32 * 1024, Number(options.maxBytes) || MAX_BACKGROUND_BYTES);
|
|
const maxEdge = Math.max(320, Number(options.maxEdge) || MAX_BACKGROUND_EDGE);
|
|
const image = await loadImageFromFile(file);
|
|
const canvas = document.createElement('canvas');
|
|
const context = canvas.getContext('2d', { alpha: false });
|
|
|
|
if (!context) {
|
|
throw new Error('Could not prepare image compression');
|
|
}
|
|
|
|
let bestDataUrl = '';
|
|
let bestBytes = Number.POSITIVE_INFINITY;
|
|
|
|
for (const edgeStep of BACKGROUND_IMAGE_EDGE_STEPS) {
|
|
const dimensions = constrainImageDimensions({
|
|
width: image.naturalWidth,
|
|
height: image.naturalHeight,
|
|
maxEdge: Math.max(320, Math.round(maxEdge * edgeStep)),
|
|
});
|
|
|
|
canvas.width = dimensions.width;
|
|
canvas.height = dimensions.height;
|
|
context.clearRect(0, 0, dimensions.width, dimensions.height);
|
|
context.fillStyle = '#f6f0e6';
|
|
context.fillRect(0, 0, dimensions.width, dimensions.height);
|
|
context.drawImage(image, 0, 0, dimensions.width, dimensions.height);
|
|
|
|
for (const quality of BACKGROUND_IMAGE_QUALITIES) {
|
|
const dataUrl = canvas.toDataURL(BACKGROUND_IMAGE_TYPE, quality);
|
|
const bytes = estimateDataUrlBytes(dataUrl);
|
|
if (bytes < bestBytes) {
|
|
bestBytes = bytes;
|
|
bestDataUrl = dataUrl;
|
|
}
|
|
if (bytes <= maxBytes) {
|
|
return dataUrl;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bestDataUrl) {
|
|
throw new Error(`Image is too large even after compression (${Math.ceil(bestBytes / 1024)} KB)`);
|
|
}
|
|
|
|
throw new Error('Could not compress image');
|
|
}
|
|
|
|
const api = {
|
|
BACKGROUND_IMAGE_TYPE,
|
|
MAX_BACKGROUND_BYTES,
|
|
MAX_BACKGROUND_EDGE,
|
|
compressImageFileForStorage,
|
|
constrainImageDimensions,
|
|
estimateDataUrlBytes,
|
|
};
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = api;
|
|
}
|
|
|
|
globalScope.TabOutBackgroundImage = api;
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window);
|