From fa0d857ec90fb5357516487776b9ab7b1f851ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ANDR=CE=98M=CE=9ED=CE=94?= Date: Sun, 26 Jun 2022 23:56:17 +0545 Subject: [PATCH] feat: implement loadImage function (#483) --- __test__/loadimage.spec.ts | 42 ++++++++++++++++++ index.d.ts | 10 +++++ index.js | 3 ++ load-image.js | 90 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) create mode 100644 __test__/loadimage.spec.ts create mode 100644 load-image.js diff --git a/__test__/loadimage.spec.ts b/__test__/loadimage.spec.ts new file mode 100644 index 00000000..86405a2b --- /dev/null +++ b/__test__/loadimage.spec.ts @@ -0,0 +1,42 @@ +import { join } from 'path' +import test from 'ava' + +import { createCanvas, Image, loadImage } from '../index' + +import { snapshotImage } from './image-snapshot' + +test('should load file src', async (t) => { + const img = await loadImage(join(__dirname, '../example/simple.png')) + t.is(img instanceof Image, true) +}) + +test('should load remote url', async (t) => { + const img = await loadImage( + 'https://raw.githubusercontent.com/Brooooooklyn/canvas/462fce53afeaee6d6b4ae5d1b407c17e2359ff7e/example/anime-girl.png', + ) + t.is(img instanceof Image, true) +}) + +test('should load data uri', async (t) => { + const img = await loadImage( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII', + ) + t.is(img instanceof Image, true) +}) + +test('should draw img', async (t) => { + const img = await loadImage( + 'https://raw.githubusercontent.com/Brooooooklyn/canvas/462fce53afeaee6d6b4ae5d1b407c17e2359ff7e/example/anime-girl.png', + ) + + // create a canvas of the same size as the image + const canvas = createCanvas(img.width, img.height) + const ctx = canvas.getContext('2d') + + // fill the canvas with the image + ctx.fillStyle = '#23eff0' + ctx.fillRect(0, 0, canvas.width, canvas.height) + ctx.drawImage(img, 250, 250) + + await snapshotImage(t, { canvas }, 'jpeg') +}) diff --git a/index.d.ts b/index.d.ts index 1c009373..d8977e98 100644 --- a/index.d.ts +++ b/index.d.ts @@ -388,3 +388,13 @@ export const enum SvgExportFlag { } export function convertSVGTextToPath(svg: Buffer | string): Buffer + +export interface LoadImageOptions { + maxRedirects?: number + requestOptions?: import('http').RequestOptions +} + +export function loadImage( + source: string | URL | Buffer | ArrayBufferLike | Uint8Array | Image, + options?: LoadImageOptions, +): Promise diff --git a/index.js b/index.js index 152065c3..b0589609 100644 --- a/index.js +++ b/index.js @@ -16,6 +16,8 @@ const { const { DOMPoint, DOMMatrix, DOMRect } = require('./geometry') +const loadImage = require('./load-image') + const StrokeJoin = { Miter: 0, Round: 1, @@ -344,4 +346,5 @@ module.exports = { DOMPoint, DOMMatrix, DOMRect, + loadImage, } diff --git a/load-image.js b/load-image.js new file mode 100644 index 00000000..0844c074 --- /dev/null +++ b/load-image.js @@ -0,0 +1,90 @@ +const fs = require('fs') +const { Image } = require('./js-binding') + +let http, https + +const MAX_REDIRECTS = 20, + REDIRECT_STATUSES = [301, 302], + DATA_URI = /^\s*data:/ + +/** + * Loads the given source into canvas Image + * @param {string|URL|Image|Buffer} source The image source to be loaded + * @param {object} options Options passed to the loader + */ +module.exports = async function loadImage(source, options = {}) { + // use the same buffer without copying if the source is a buffer + if (Buffer.isBuffer(source)) return createImage(source) + // construct a buffer if the source is buffer-like + if (isBufferLike(source)) return createImage(Buffer.from(source)) + // if the source is Image instance, copy the image src to new image + if (source instanceof Image) return createImage(source.src) + // if source is string and in data uri format, construct image using data uri + if (typeof source === 'string' && DATA_URI.test(source)) { + const commaIdx = source.indexOf(',') + const encoding = source.lastIndexOf('base64', commaIdx) < 0 ? 'utf-8' : 'base64' + const data = Buffer.from(source.slice(commaIdx + 1), encoding) + return createImage(data) + } + // if source is a string or URL instance + if (typeof source === 'string' || source instanceof URL) { + // if the source exists as a file, construct image from that file + if (fs.existsSync(source)) { + return createImage(await fs.promises.readFile(source)) + } else { + // the source is a remote url here + source = !(source instanceof URL) ? new URL(source) : source + // attempt to download the remote source and construct image + const data = await new Promise((resolve, reject) => + makeRequest(source, resolve, reject, options.maxRedirects ?? MAX_REDIRECTS, options.requestOptions), + ) + return createImage(data) + } + } + + // throw error as dont support that source + throw new TypeError('unsupported image source') +} + +function makeRequest(url, resolve, reject, redirectCount, requestOptions) { + const isHttps = url.protocol === 'https:' + // lazy load the lib + const lib = isHttps ? (!https ? (https = require('https')) : https) : !http ? (http = require('http')) : http + + lib.get(url, requestOptions ?? {}, (res) => { + const shouldRedirect = REDIRECT_STATUSES.includes(res.statusCode) && typeof res.headers.location === 'string' + if (shouldRedirect && redirectCount > 0) + return makeRequest(res.headers.location, resolve, reject, redirectCount - 1, requestOptions) + if (typeof res.statusCode === 'number' && res.statusCode < 200 && res.statusCode >= 300) { + return reject(new Error(`remote source rejected with status code ${res.statusCode}`)) + } + + consumeStream(res).then(resolve, reject) + }) +} + +// use stream/consumers in the future? +function consumeStream(res) { + return new Promise((resolve, reject) => { + const chunks = [] + + res.on('data', (chunk) => chunks.push(chunk)) + res.on('end', () => resolve(Buffer.concat(chunks))) + res.on('error', reject) + }) +} + +function createImage(src) { + const image = new Image() + image.src = src + return image +} + +function isBufferLike(src) { + return ( + Array.isArray(src) || + src instanceof ArrayBuffer || + src instanceof SharedArrayBuffer || + src instanceof Object.getPrototypeOf(Uint8Array) + ) +}