From a3724676188d40015f19bd9f54ff11bc76d9387a Mon Sep 17 00:00:00 2001 From: benitoalba Date: Tue, 14 Jul 2026 00:27:15 +0200 Subject: [PATCH] Support Node.js object URLs as image sources --- CHANGELOG.md | 1 + lib/image.js | 33 +++++++++++++++++++++++---------- test/image.test.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b367a99c..26fb99743 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed ### Added ### Fixed +* Load images from Node.js object URLs (#2525) 3.2.3 ================== diff --git a/lib/image.js b/lib/image.js index a6c81ba83..a8713b61f 100644 --- a/lib/image.js +++ b/lib/image.js @@ -13,6 +13,7 @@ const bindings = require('./bindings') const Image = module.exports = bindings.Image const util = require('util') +const { resolveObjectURL } = require('node:buffer') const { GetSource, SetSource } = bindings @@ -20,6 +21,7 @@ Object.defineProperty(Image.prototype, 'src', { /** * src setter. Valid values: * * `data:` URI + * * Node.js object URL * * Local file path * * HTTP or HTTPS URL * * Buffer containing image data (i.e. not a `data:` URI stored in a Buffer) @@ -28,6 +30,14 @@ Object.defineProperty(Image.prototype, 'src', { * @api public */ set (val) { + const onerror = err => { + if (typeof this.onerror === 'function') { + this.onerror(err) + } else { + throw err + } + } + if (typeof val === 'string') { if (/^\s*data:/.test(val)) { // data: URI const commaI = val.indexOf(',') @@ -35,15 +45,19 @@ Object.defineProperty(Image.prototype, 'src', { const isBase64 = val.lastIndexOf('base64', commaI) !== -1 const content = val.slice(commaI + 1) setSource(this, Buffer.from(content, isBase64 ? 'base64' : 'utf8'), val) - } else if (/^\s*https?:\/\//.test(val)) { // remote URL - const onerror = err => { - if (typeof this.onerror === 'function') { - this.onerror(err) - } else { - throw err - } + } else if (/^\s*blob:nodedata:/.test(val)) { // Node.js object URL + const blob = resolveObjectURL(val) + if (!blob) { + onerror(new Error('Invalid object URL')) + return } + blob.arrayBuffer() + .then(data => { + setSource(this, Buffer.from(data), val) + }) + .catch(onerror) + } else if (/^\s*https?:\/\//.test(val)) { // remote URL fetch(val, { method: 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36' } @@ -64,9 +78,8 @@ Object.defineProperty(Image.prototype, 'src', { } else if (Buffer.isBuffer(val)) { setSource(this, val) } else { - const err = new Error("Invalid image source") - if (typeof this.onerror === 'function') this.onerror(err) - else throw err + const err = new Error('Invalid image source') + onerror(err) } }, diff --git a/test/image.test.js b/test/image.test.js index de7c9824f..defb52115 100644 --- a/test/image.test.js +++ b/test/image.test.js @@ -10,6 +10,8 @@ const assertRejects = require('assert-rejects') const fs = require('fs') const path = require('path') const os = require('os') +const { Blob } = require('node:buffer') +const { URL } = require('node:url') const { createCanvas, loadImage, rsvgVersion, Image } = require('../') const HAVE_SVG = rsvgVersion !== undefined @@ -90,6 +92,34 @@ describe('Image', function () { }) }) + it('loads PNG object URL', async function () { + const objectURL = URL.createObjectURL(new Blob([ + fs.readFileSync(pngClock) + ], { type: 'image/png' })) + + try { + const img = await loadImage(objectURL) + assert.strictEqual(img.src, objectURL) + assert.strictEqual(img.width, 320) + assert.strictEqual(img.height, 320) + + const canvas = createCanvas(img.width, img.height) + const ctx = canvas.getContext('2d') + assert.doesNotThrow(() => ctx.drawImage(img, 0, 0)) + } finally { + URL.revokeObjectURL(objectURL) + } + }) + + it('rejects revoked object URLs', async function () { + const objectURL = URL.createObjectURL(new Blob([ + fs.readFileSync(pngClock) + ], { type: 'image/png' })) + URL.revokeObjectURL(objectURL) + + await assert.rejects(loadImage(objectURL), /Invalid object URL/) + }) + it('detects invalid PNG', function (done) { if (process.platform === 'win32') this.skip() // TODO const img = new Image()