Skip to content

Commit

Permalink
feat: implement loadImage function (#483)
Browse files Browse the repository at this point in the history
  • Loading branch information
twlite authored Jun 26, 2022
1 parent d60ee86 commit fa0d857
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 0 deletions.
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)
)
}

0 comments on commit fa0d857

Please sign in to comment.