Skip to content

Commit

Permalink
feat: add stream and alt support for Image (#486)
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite authored Jun 28, 2022
1 parent fa0d857 commit 671c4b1
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 13 deletions.
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

0 comments on commit 671c4b1

Please sign in to comment.