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(loadImage): add stream and alt support #486

Merged
merged 1 commit into from
Jun 28, 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
13 changes: 13 additions & 0 deletions __test__/loadimage.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { join } from 'path'
import test from 'ava'
import fs from 'fs'

import { createCanvas, Image, loadImage } from '../index'

Expand All @@ -10,6 +11,18 @@ test('should load file src', async (t) => {
t.is(img instanceof Image, true)
})

test('should load file stream', async (t) => {
const img = await loadImage(fs.createReadStream(join(__dirname, '../example/simple.png')))
t.is(img instanceof Image, true)
})

test('should load image with alt', async (t) => {
const img = await loadImage(join(__dirname, '../example/simple.png'), {
alt: 'demo-image',
})
t.is(img.alt, 'demo-image')
})

test('should load remote url', async (t) => {
const img = await loadImage(
'https://raw.githubusercontent.com/Brooooooklyn/canvas/462fce53afeaee6d6b4ae5d1b407c17e2359ff7e/example/anime-girl.png',
Expand Down
7 changes: 4 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,11 +390,12 @@ export const enum SvgExportFlag {
export function convertSVGTextToPath(svg: Buffer | string): Buffer

export interface LoadImageOptions {
maxRedirects?: number
requestOptions?: import('http').RequestOptions
alt?: string,
maxRedirects?: number,
requestOptions?: import('http').RequestOptions,
}

export function loadImage(
source: string | URL | Buffer | ArrayBufferLike | Uint8Array | Image,
source: string | URL | Buffer | ArrayBufferLike | Uint8Array | Image | import('stream').Readable,
options?: LoadImageOptions,
): Promise<Image>
24 changes: 14 additions & 10 deletions load-image.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const fs = require('fs')
const { Image } = require('./js-binding')
const { Readable } = require('stream')

let http, https

const MAX_REDIRECTS = 20,
REDIRECT_STATUSES = [301, 302],
REDIRECT_STATUSES = new Set([301, 302]),
DATA_URI = /^\s*data:/

/**
Expand All @@ -13,32 +14,34 @@ const MAX_REDIRECTS = 20,
* @param {object} options Options passed to the loader
*/
module.exports = async function loadImage(source, options = {}) {
// load readable stream as image
if (source instanceof Readable) return createImage(await consumeStream(source), options.alt)
// use the same buffer without copying if the source is a buffer
if (Buffer.isBuffer(source)) return createImage(source)
if (Buffer.isBuffer(source)) return createImage(source, options.alt)
// construct a buffer if the source is buffer-like
if (isBufferLike(source)) return createImage(Buffer.from(source))
if (isBufferLike(source)) return createImage(Buffer.from(source), options.alt)
// if the source is Image instance, copy the image src to new image
if (source instanceof Image) return createImage(source.src)
if (source instanceof Image) return createImage(source.src, options.alt)
// 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)
return createImage(data, options.alt)
}
// 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))
return createImage(await fs.promises.readFile(source), options.alt)
} 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)
return createImage(data, options.alt)
}
}

Expand All @@ -52,10 +55,10 @@ function makeRequest(url, resolve, reject, redirectCount, requestOptions) {
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'
const shouldRedirect = REDIRECT_STATUSES.has(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) {
if (typeof res.statusCode === 'number' && (res.statusCode < 200 || res.statusCode >= 300)) {
return reject(new Error(`remote source rejected with status code ${res.statusCode}`))
}

Expand All @@ -74,9 +77,10 @@ function consumeStream(res) {
})
}

function createImage(src) {
function createImage(src, alt) {
const image = new Image()
image.src = src
if (typeof alt === 'string') image.alt = alt
return image
}

Expand Down