UNPKG

@beenotung/tslib

Version:
506 lines (505 loc) 16.6 kB
"use strict"; 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; }); }