cnf
2025-05-10 386fa0eca75ddc88165f9b73038f2a2239e1072e
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
  }
}