Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement loadImage function #483

Merged
merged 7 commits into from
Jun 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions __test__/loadimage.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})
10 changes: 10 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Image>
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const {

const { DOMPoint, DOMMatrix, DOMRect } = require('./geometry')

const loadImage = require('./load-image')

const StrokeJoin = {
Miter: 0,
Round: 1,
Expand Down Expand Up @@ -344,4 +346,5 @@ module.exports = {
DOMPoint,
DOMMatrix,
DOMRect,
loadImage,
}
90 changes: 90 additions & 0 deletions load-image.js
Original file line number Diff line number Diff line change
@@ -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)
)
}