@beenotung/tslib
Version:
utils library in Typescript
506 lines (505 loc) • 16.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ResizeTypes = exports.convertHeifFile = void 0;
exports.imageToCanvas = imageToCanvas;
exports.imageToBase64 = imageToBase64;
exports.convertHeicFile = convertHeicFile;
exports.base64ToImage = base64ToImage;
exports.checkBase64ImagePrefix = checkBase64ImagePrefix;
exports.base64ToCanvas = base64ToCanvas;
exports.resizeBase64Image = resizeBase64Image;
exports.getWidthHeightFromBase64 = getWidthHeightFromBase64;
exports.resizeWithRatio = resizeWithRatio;
exports.resizeBase64WithRatio = resizeBase64WithRatio;
exports.resizeImage = resizeImage;
exports.transformCentered = transformCentered;
exports.rotateImage = rotateImage;
exports.flipImage = flipImage;
exports.flipImageX = flipImageX;
exports.flipImageY = flipImageY;
exports.dataURItoMimeType = dataURItoMimeType;
exports.dataURItoBlob = dataURItoBlob;
exports.dataURItoFile = dataURItoFile;
exports.compressImage = compressImage;
exports.compressImageToBase64 = compressImageToBase64;
exports.canvasToBlob = canvasToBlob;
exports.compressImageToBlob = compressImageToBlob;
exports.toImage = toImage;
exports.compressMobilePhoto = compressMobilePhoto;
const file_1 = require("./file");
const result_1 = require("./result");
const size_1 = require("./size");
/**
* reference : https://stackoverflow.com/questions/20958078/resize-a-base-64-image-in-javascript-without-using-canvas
* */
function imageToCanvas(img, width, height) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (ctx === null) {
throw new Error('unsupported');
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
return canvas;
}
function imageToBase64(img, width, height) {
return imageToCanvas(img, width, height).toDataURL();
}
async function convertHeicFile(file) {
let heic2any;
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
heic2any = require('heic2any');
}
catch (error) {
// explicitly try-catch around the require to avoid bundle-time error
throw new Error('optional dependency "heic2any" is not installed');
}
const blob = await heic2any({ blob: file });
const blobs = Array.isArray(blob) ? blob : [blob];
const type = blobs[0].type;
let filename = file.name;
{
const ext = type.split('/')[1].split(';')[0];
const parts = filename.split('.');
parts.pop();
parts.push(ext);
filename = parts.join('.');
}
file = new File(blobs, filename, { type, lastModified: file.lastModified });
return file;
}
/** @alias convertHeicFile */
exports.convertHeifFile = convertHeicFile;
function is_heic2any_installed() {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('heic2any');
return true;
}
catch (error) {
// heic2any is not installed
return false;
}
}
async function base64ToImage(data) {
if (data.startsWith('data:image/heic') ||
data.startsWith('data:image/heif')) {
if (is_heic2any_installed()) {
const res = await fetch(data);
const blob = await res.blob();
const file = new File([blob], 'image.heic', { type: 'image/heic' });
return toImage(file);
}
console.warn('heic2any is not installed, skip format conversion');
}
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = e => reject(e);
image.src = data;
});
}
/**
* TODO check if there are exceptions
* */
function checkBase64ImagePrefix(s) {
return typeof s === 'string' && s.startsWith('/9j/')
? 'data:image/jpeg;base64,' + s
: s;
}
/**
* data type conversion
* also work for resizing
* FIXME wrap width and height into options object
* */
async function base64ToCanvas(data, width, height) {
const image = await base64ToImage(data);
let w;
let h;
if (width && height) {
w = width;
h = height;
}
else if (!width && !height) {
w = image.naturalWidth;
h = image.naturalHeight;
}
else if (width) {
// height is not defined
w = width;
h = (image.naturalHeight / image.naturalWidth) * width;
}
else if (height) {
// width is not defined
w = (image.naturalWidth / image.naturalHeight) * height;
h = height;
}
else {
throw new Error('logic error, missing edge case:' + JSON.stringify({ width, height }));
}
return imageToCanvas(image, w, h);
}
async function resizeBase64Image(data, targetWidth, targetHeight) {
return (await base64ToCanvas(data, targetWidth, targetHeight)).toDataURL();
}
async function getWidthHeightFromBase64(data) {
const image = await base64ToImage(data);
return {
width: image.naturalWidth,
height: image.naturalHeight,
};
}
exports.ResizeTypes = {
/* with-in the given area, maybe smaller */
with_in: 'with_in',
/* at least as large as the given area, maybe larger */
at_least: 'at_least',
};
function resizeWithRatio(oriSize, targetSize, mode) {
const widthRate = targetSize.width / oriSize.width;
const heightRate = targetSize.height / oriSize.height;
let rate;
switch (mode) {
case exports.ResizeTypes.with_in:
rate = Math.min(widthRate, heightRate);
break;
case exports.ResizeTypes.at_least:
rate = Math.max(widthRate, heightRate);
break;
default:
throw new TypeError(`unsupported type: ${mode}`);
}
return {
width: oriSize.width * rate,
height: oriSize.height * rate,
};
}
async function resizeBase64WithRatio(data, preferredSize, mode) {
const image = await base64ToImage(data);
const targetSize = resizeWithRatio({
width: image.naturalWidth,
height: image.naturalHeight,
}, preferredSize, mode);
return imageToBase64(image, targetSize.width, targetSize.height);
}
// reference: image-file-to-base64-exif
function getNewScale(image, maxWidth, maxHeight) {
if (image.width <= maxWidth && image.height <= maxHeight) {
return 1;
}
if (image.width > image.height) {
return image.width / maxWidth;
}
else {
return image.height / maxHeight;
}
}
function resizeImage(image, maxWidth = image.width, maxHeight = image.height, mimeType, quality) {
const scale = getNewScale(image, maxWidth, maxHeight);
const scaledWidth = image.width / scale;
const scaledHeight = image.height / scale;
const canvas = document.createElement('canvas');
canvas.width = scaledWidth;
canvas.height = scaledHeight;
const context = canvas.getContext('2d');
if (context === null) {
throw new Error('not supported');
}
context.drawImage(image, 0, 0, scaledWidth, scaledHeight);
if (mimeType) {
return canvas.toDataURL(mimeType, quality || 1);
}
else {
return canvas.toDataURL();
}
}
function transformCentered(image, flipXY, f) {
const canvas = document.createElement('canvas');
const imageWidth = image.naturalWidth || image.width;
const imageHeight = image.naturalHeight || image.height;
if (flipXY) {
canvas.width = imageHeight;
canvas.height = imageWidth;
}
else {
canvas.width = imageWidth;
canvas.height = imageHeight;
}
const ctx = canvas.getContext('2d');
if (ctx === null) {
throw new Error('not supported');
}
ctx.translate(canvas.width * 0.5, canvas.height * 0.5);
f(ctx);
ctx.translate(-imageWidth * 0.5, -imageHeight * 0.5);
ctx.drawImage(image, 0, 0);
// return canvas.toDataURL();
return canvas;
}
function rotateImage(image) {
return transformCentered(image, true, ctx => ctx.rotate(0.5 * Math.PI));
}
function flipImage(image, direction) {
switch (direction) {
case undefined:
case 'X':
case 'horizontal':
return flipImageX(image);
case 'Y':
case 'vertical':
return flipImageY(image);
default:
throw new Error('unsupported direction: ' + direction);
}
}
function flipImageX(image) {
return transformCentered(image, false, ctx => ctx.scale(-1, 1));
}
function flipImageY(image) {
return transformCentered(image, false, ctx => ctx.scale(1, -1));
}
/**
* extract mime type from base64/URLEncoded data component
* e.g. data:image/jpeg;base64,... -> image/jpeg
* */
function dataURItoMimeType(dataURI) {
const idx = dataURI.indexOf(',');
if (idx === -1) {
throw new Error('data uri prefix not found');
}
const prefix = dataURI.substring(0, idx);
const [mimeType] = prefix.replace(/^data:/, '').split(';');
return mimeType;
}
/**
* convert base64/URLEncoded data component to raw binary data held in a string
* e.g. data:image/jpeg;base64,...
* */
function dataURItoBlob(dataURI) {
const [format, payload] = dataURI.split(',');
// const [mimeType, encodeType]
const [mimeType] = format.replace(/^data:/, '').split(';');
let byteString;
if (dataURI.startsWith('data:')) {
byteString = atob(payload);
}
else {
byteString = unescape(payload);
}
const n = byteString.length;
const buffer = new Uint8Array(n);
for (let i = 0; i < n; i++) {
buffer[i] = byteString.charCodeAt(i);
}
return new Blob([buffer], { type: mimeType });
}
function dataURItoFile(dataURI, originalFile) {
const blob = dataURItoBlob(dataURI);
let filename = removeExtname(originalFile?.name || 'image');
const ext = blob.type.split('/').pop();
filename += '.' + ext;
return new File([blob], filename, {
type: blob.type,
lastModified: originalFile?.lastModified || Date.now(),
});
}
function removeExtname(filename) {
return filename.replace(/\.(jpg|jpeg|png|gif|bmp|webp)$/i, '');
}
/** simplified version of compressImageToBase64() / compressImageToBlob() */
function compressImage(image, mimeType, quality = 0.8) {
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
if (ctx === null) {
throw new Error('not supported');
}
ctx.drawImage(image, 0, 0);
if (mimeType) {
return canvas.toDataURL(mimeType, quality);
}
const all = [
canvas.toDataURL('image/png', quality),
canvas.toDataURL('image/jpeg', quality),
canvas.toDataURL('image/webp', quality),
];
const min = all.sort((a, b) => a.length - b.length)[0];
return min;
}
function populateCompressArgs(args) {
const image = args.image;
const canvas = args.canvas || document.createElement('canvas');
const ctx = args.ctx ||
canvas.getContext('2d') ||
(() => {
throw new Error('not supported');
})();
let maximumSize = args.maximumSize;
let quality = args.quality;
if (!maximumSize && !quality) {
maximumSize = 768 * size_1.KB; // 768KB
quality = 0.8;
}
return {
image,
canvas,
ctx,
maximumSize,
quality,
};
}
function compressImageToBase64(args) {
const { image, canvas, ctx, maximumSize, quality } = populateCompressArgs({
...args,
maximumSize: args.maximumLength,
});
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
let mimeType;
let dataURL;
if (args.mimeType) {
mimeType = args.mimeType;
dataURL = canvas.toDataURL(mimeType, quality);
}
else {
const min = ['image/png', 'image/jpeg', 'image/webp']
.map(mimeType => {
const base64 = canvas.toDataURL(mimeType, quality);
const size = base64ToSize(base64);
return { mimeType, base64, size };
})
.sort((a, b) => a.size - b.size)[0];
mimeType = min.mimeType;
dataURL = min.base64;
}
if (!maximumSize) {
return dataURL;
}
const w_h_ratio = canvas.width / canvas.height;
for (;;) {
const binSize = base64ToSize(dataURL);
if (binSize <= maximumSize || canvas.width == 0 || canvas.height == 0) {
break;
}
const ratio = Math.sqrt(maximumSize / dataURL.length);
let new_width = Math.round(canvas.width * ratio);
let new_height = Math.round(new_width / w_h_ratio);
if (new_width === canvas.width && new_height === canvas.height) {
if (new_width > new_height) {
new_width--;
}
else if (new_height > new_width) {
new_height--;
}
else {
new_width--;
new_height--;
}
}
canvas.width = new_width;
canvas.height = new_height;
ctx.drawImage(image, 0, 0, new_width, new_height);
dataURL = canvas.toDataURL(mimeType, quality);
}
return dataURL;
}
function canvasToBlob(canvas, mimeType, quality) {
return new Promise((resolve, reject) => canvas.toBlob(blob => {
if (blob) {
resolve(blob);
}
else {
reject('not supported');
}
}, mimeType, quality));
}
async function compressImageToBlob(args) {
const { image, canvas, ctx, maximumSize, quality } = populateCompressArgs(args);
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0);
let mimeType;
let blob;
if (args.mimeType) {
mimeType = args.mimeType;
blob = await canvasToBlob(canvas, mimeType, quality);
}
else {
const all = await Promise.all([
canvasToBlob(canvas, 'image/png', quality),
canvasToBlob(canvas, 'image/jpeg', quality),
canvasToBlob(canvas, 'image/webp', quality),
]);
blob = all.sort((a, b) => a.size - b.size)[0];
mimeType = blob.type;
}
if (!maximumSize) {
return blob;
}
for (; blob.size > maximumSize;) {
const ratio = Math.sqrt(maximumSize / blob.size);
const new_width = Math.round(canvas.width * ratio);
const new_height = Math.round(canvas.height * ratio);
if (new_width === canvas.width && new_height === canvas.height) {
break;
}
canvas.width = new_width;
canvas.height = new_height;
ctx.drawImage(image, 0, 0, new_width, new_height);
blob = await canvasToBlob(canvas, mimeType, quality);
}
return blob;
}
function toImage(image) {
if (typeof image === 'string') {
// base64
return base64ToImage(image);
}
if (image instanceof File) {
if (image.type == 'image/heic' || image.type == 'image/heif') {
if (is_heic2any_installed()) {
return convertHeicFile(image).then(file => toImage(file));
}
console.warn('heic2any is not installed, skip format conversion');
}
return (0, file_1.fileToBase64String)(image).then(base64 => toImage(base64));
}
if (image instanceof HTMLImageElement) {
return image;
}
console.error('unknown image type:', image);
throw new TypeError('unknown image type');
}
const DefaultMaximumMobilePhotoSize = 300 * size_1.KB; // 300KB
const base64Overhead = 4 / 3;
function base64ToSize(base64) {
return (base64.length - base64.indexOf(',') - 1) / base64Overhead;
}
async function compressMobilePhoto(args) {
const maximumLength = args.maximumSize || DefaultMaximumMobilePhotoSize;
const originalSize = args.image instanceof File
? args.image.size
: typeof args.image === 'string'
? base64ToSize(args.image)
: null;
return (0, result_1.then)(toImage(args.image), image => {
const base64 = compressImageToBase64({
image,
maximumLength,
mimeType: args.mimeType,
quality: args.quality,
});
const newSize = base64ToSize(base64);
if (originalSize && originalSize <= newSize) {
if (typeof args.image === 'string')
return args.image;
if (args.image instanceof File)
return (0, file_1.fileToBase64String)(args.image);
}
return base64;
});
}