258 lines
7.0 KiB
JavaScript
258 lines
7.0 KiB
JavaScript
import fs from 'fs';
|
|
import url from 'url';
|
|
import path from 'path';
|
|
import _PNG from '@react-pdf/png-js';
|
|
import _JPEG from 'jay-peg';
|
|
|
|
class PNG {
|
|
data;
|
|
width;
|
|
height;
|
|
format;
|
|
constructor(data) {
|
|
const png = new _PNG(data);
|
|
this.data = data;
|
|
this.width = png.width;
|
|
this.height = png.height;
|
|
this.format = 'png';
|
|
}
|
|
static isValid(data) {
|
|
try {
|
|
return !!new PNG(data);
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
class JPEG {
|
|
data;
|
|
width;
|
|
height;
|
|
format;
|
|
constructor(data) {
|
|
this.data = data;
|
|
this.format = 'jpeg';
|
|
this.width = 0;
|
|
this.height = 0;
|
|
if (data.readUInt16BE(0) !== 0xffd8) {
|
|
throw new Error('SOI not found in JPEG');
|
|
}
|
|
const markers = _JPEG.decode(this.data);
|
|
let orientation;
|
|
for (let i = 0; i < markers.length; i += 1) {
|
|
const marker = markers[i];
|
|
if (marker.name === 'EXIF' && marker.entries.orientation) {
|
|
orientation = marker.entries.orientation;
|
|
}
|
|
if (marker.name === 'SOF') {
|
|
this.width ||= marker.width;
|
|
this.height ||= marker.height;
|
|
}
|
|
}
|
|
if (orientation > 4) {
|
|
[this.width, this.height] = [this.height, this.width];
|
|
}
|
|
}
|
|
static isValid(data) {
|
|
return data && Buffer.isBuffer(data) && data.readUInt16BE(0) === 0xffd8;
|
|
}
|
|
}
|
|
|
|
const createCache = ({ limit = 100 } = {}) => {
|
|
let cache = {};
|
|
let keys = [];
|
|
return {
|
|
get: (key) => (key ? cache[key] : null),
|
|
set: (key, value) => {
|
|
keys.push(key);
|
|
if (keys.length > limit) {
|
|
delete cache[keys.shift()];
|
|
}
|
|
cache[key] = value;
|
|
},
|
|
reset: () => {
|
|
cache = {};
|
|
keys = [];
|
|
},
|
|
length: () => keys.length,
|
|
};
|
|
};
|
|
|
|
const IMAGE_CACHE = createCache({ limit: 30 });
|
|
const isBuffer = Buffer.isBuffer;
|
|
const isBlob = (src) => {
|
|
return typeof Blob !== 'undefined' && src instanceof Blob;
|
|
};
|
|
const isDataImageSrc = (src) => {
|
|
return 'data' in src;
|
|
};
|
|
const isBase64Src = (imageSrc) => 'uri' in imageSrc &&
|
|
/^data:image\/[a-zA-Z]*;base64,[^"]*/g.test(imageSrc.uri);
|
|
const getAbsoluteLocalPath = (src) => {
|
|
const { protocol, auth, host, port, hostname, path: pathname, } = url.parse(src);
|
|
const absolutePath = pathname ? path.resolve(pathname) : undefined;
|
|
if ((protocol && protocol !== 'file:') || auth || host || port || hostname) {
|
|
return undefined;
|
|
}
|
|
return absolutePath;
|
|
};
|
|
const fetchLocalFile = (src) => new Promise((resolve, reject) => {
|
|
try {
|
|
if (false) ;
|
|
const absolutePath = getAbsoluteLocalPath(src.uri);
|
|
if (!absolutePath) {
|
|
reject(new Error(`Cannot fetch non-local path: ${src}`));
|
|
return;
|
|
}
|
|
fs.readFile(absolutePath, (err, data) => err ? reject(err) : resolve(data));
|
|
}
|
|
catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
const fetchRemoteFile = async (src) => {
|
|
const { method = 'GET', headers, body, credentials } = src;
|
|
const response = await fetch(src.uri, {
|
|
method,
|
|
headers,
|
|
body,
|
|
credentials,
|
|
});
|
|
const buffer = await response.arrayBuffer();
|
|
return Buffer.from(buffer);
|
|
};
|
|
const isValidFormat = (format) => {
|
|
const lower = format.toLowerCase();
|
|
return lower === 'jpg' || lower === 'jpeg' || lower === 'png';
|
|
};
|
|
const guessFormat = (buffer) => {
|
|
let format;
|
|
if (JPEG.isValid(buffer)) {
|
|
format = 'jpg';
|
|
}
|
|
else if (PNG.isValid(buffer)) {
|
|
format = 'png';
|
|
}
|
|
return format;
|
|
};
|
|
function getImage(body, format) {
|
|
switch (format.toLowerCase()) {
|
|
case 'jpg':
|
|
case 'jpeg':
|
|
return new JPEG(body);
|
|
case 'png':
|
|
return new PNG(body);
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
const resolveBase64Image = async ({ uri }) => {
|
|
const match = /^data:image\/([a-zA-Z]*);base64,([^"]*)/g.exec(uri);
|
|
if (!match)
|
|
throw new Error(`Invalid base64 image: ${uri}`);
|
|
const format = match[1];
|
|
const data = match[2];
|
|
if (!isValidFormat(format))
|
|
throw new Error(`Base64 image invalid format: ${format}`);
|
|
return getImage(Buffer.from(data, 'base64'), format);
|
|
};
|
|
const resolveImageFromData = async (src) => {
|
|
if (src.data && src.format) {
|
|
return getImage(src.data, src.format);
|
|
}
|
|
throw new Error(`Invalid data given for local file: ${JSON.stringify(src)}`);
|
|
};
|
|
const resolveBufferImage = async (buffer) => {
|
|
const format = guessFormat(buffer);
|
|
if (format) {
|
|
return getImage(buffer, format);
|
|
}
|
|
return null;
|
|
};
|
|
const resolveBlobImage = async (blob) => {
|
|
const { type } = blob;
|
|
if (!type || type === 'application/octet-stream') {
|
|
const arrayBuffer = await blob.arrayBuffer();
|
|
const buffer = Buffer.from(arrayBuffer);
|
|
return resolveBufferImage(buffer);
|
|
}
|
|
if (!type.startsWith('image/')) {
|
|
throw new Error(`Invalid blob type: ${type}`);
|
|
}
|
|
const format = type.replace('image/', '');
|
|
if (!isValidFormat(format)) {
|
|
throw new Error(`Invalid blob type: ${type}`);
|
|
}
|
|
const buffer = await blob.arrayBuffer();
|
|
return getImage(Buffer.from(buffer), format);
|
|
};
|
|
const getImageFormat = (body) => {
|
|
const isPng = body[0] === 137 &&
|
|
body[1] === 80 &&
|
|
body[2] === 78 &&
|
|
body[3] === 71 &&
|
|
body[4] === 13 &&
|
|
body[5] === 10 &&
|
|
body[6] === 26 &&
|
|
body[7] === 10;
|
|
const isJpg = body[0] === 255 && body[1] === 216 && body[2] === 255;
|
|
let extension = '';
|
|
if (isPng) {
|
|
extension = 'png';
|
|
}
|
|
else if (isJpg) {
|
|
extension = 'jpg';
|
|
}
|
|
else {
|
|
throw new Error('Not valid image extension');
|
|
}
|
|
return extension;
|
|
};
|
|
const resolveImageFromUrl = async (src) => {
|
|
const data = getAbsoluteLocalPath(src.uri)
|
|
? await fetchLocalFile(src)
|
|
: await fetchRemoteFile(src);
|
|
const format = getImageFormat(data);
|
|
return getImage(data, format);
|
|
};
|
|
const getCacheKey = (src) => {
|
|
if (isBlob(src) || isBuffer(src))
|
|
return null;
|
|
if (isDataImageSrc(src))
|
|
return src.data.toString();
|
|
return src.uri;
|
|
};
|
|
const resolveImage = (src, { cache = true } = {}) => {
|
|
let image;
|
|
const cacheKey = getCacheKey(src);
|
|
if (isBlob(src)) {
|
|
image = resolveBlobImage(src);
|
|
}
|
|
else if (isBuffer(src)) {
|
|
image = resolveBufferImage(src);
|
|
}
|
|
else if (cache && IMAGE_CACHE.get(cacheKey)) {
|
|
return IMAGE_CACHE.get(cacheKey);
|
|
}
|
|
else if (isBase64Src(src)) {
|
|
image = resolveBase64Image(src);
|
|
}
|
|
else if (isDataImageSrc(src)) {
|
|
image = resolveImageFromData(src);
|
|
}
|
|
else {
|
|
image = resolveImageFromUrl(src);
|
|
}
|
|
if (!image) {
|
|
throw new Error('Cannot resolve image');
|
|
}
|
|
if (cache && cacheKey) {
|
|
IMAGE_CACHE.set(cacheKey, image);
|
|
}
|
|
return image;
|
|
};
|
|
|
|
export { resolveImage as default };
|