generated from napi-rs/package-template
-
-
Notifications
You must be signed in to change notification settings - Fork 76
/
load-image.js
141 lines (127 loc) · 4.87 KB
/
load-image.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
const fs = require('fs')
const { Readable } = require('stream')
const { URL } = require('url')
const { Image } = require('./js-binding')
let http, https
const MAX_REDIRECTS = 20
const REDIRECT_STATUSES = new Set([301, 302])
/**
* 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) || source instanceof Uint8Array) return createImage(source, options.alt)
// load readable stream as image
if (source instanceof Readable) return createImage(await consumeStream(source), options.alt)
// construct a Uint8Array if the source is ArrayBuffer or SharedArrayBuffer
if (source instanceof ArrayBuffer || source instanceof SharedArrayBuffer)
return createImage(new Uint8Array(source), options.alt)
// construct a buffer if the source is buffer-like
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, options.alt)
// if source is string and in data uri format, construct image using data uri
if (typeof source === 'string' && source.trimStart().startsWith('data:')) {
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, options.alt)
}
// if source is a string or URL instance
if (typeof source === 'string') {
// if the source exists as a file, construct image from that file
if ((!source.startsWith('http') && !source.startsWith('https')) && await exists(source)) {
return createImage(source, options.alt)
} else {
// the source is a remote url here
source = new URL(source)
// attempt to download the remote source and construct image
const data = await new Promise((resolve, reject) =>
makeRequest(
source,
resolve,
reject,
typeof options.maxRedirects === 'number' && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS,
options.requestOptions,
),
)
return createImage(data, options.alt)
}
}
if (source instanceof URL) {
if (source.protocol === 'file:') {
// remove the leading slash on windows
return createImage(process.platform === 'win32' ? source.pathname.substring(1) : source.pathname, options.alt)
} else {
const data = await new Promise((resolve, reject) =>
makeRequest(
source,
resolve,
reject,
typeof options.maxRedirects === 'number' && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS,
options.requestOptions,
),
)
return createImage(data, options.alt)
}
}
// throw error as don't 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.toString(), requestOptions || {}, (res) => {
try {
const shouldRedirect = REDIRECT_STATUSES.has(res.statusCode) && typeof res.headers.location === 'string'
if (shouldRedirect && redirectCount > 0)
return makeRequest(
new URL(res.headers.location, url.origin),
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)
} catch (err) {
reject(err)
}
})
.on('error', 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, alt) {
return new Promise((resolve, reject) => {
const image = new Image()
if (typeof alt === 'string') image.alt = alt
image.onload = () => resolve(image)
image.onerror = (e) => reject(e)
image.src = src
})
}
function isBufferLike(src) {
return (src && src.type === 'Buffer') || Array.isArray(src)
}
async function exists(path) {
try {
await fs.promises.access(path, fs.constants.F_OK)
return true
} catch {
return false
}
}