Skip to content

Commit c881a18

Browse files
Use native decoding of base64 in Node.js (#269)
* Make decoding of base64 data URIs faster I saw in microsoft/vscode-js-debug#1911 that base64 decoding of a data URI was taking a bit of time. This PR feature-detects the presence of a global `Buffer` to use native decoding when running in Node.js, which is about 25x faster on a 10MB data URI than the JS implementation in the library. I have a bit of a hack in order to test both paths when running tests, happy to change it if desired :) * use conditional exports * fix test import * fix node test import, and set stringToBuffer encoding * Create few-adults-rhyme.md --------- Co-authored-by: Nathan Rajlich <[email protected]>
1 parent c3c405e commit c881a18

File tree

6 files changed

+117
-61
lines changed

6 files changed

+117
-61
lines changed

.changeset/few-adults-rhyme.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"data-uri-to-buffer": patch
3+
---
4+
5+
Use native Buffer decoding in Node.js

packages/data-uri-to-buffer/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
"description": "Create an ArrayBuffer instance from a Data URI string",
55
"main": "./dist/index.js",
66
"types": "./dist/index.d.ts",
7+
"exports": {
8+
"node": "./dist/node.js",
9+
"default": "./dist/index.js"
10+
},
711
"files": [
812
"dist"
913
],
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
export interface ParsedDataURI {
2+
type: string;
3+
typeFull: string;
4+
charset: string;
5+
buffer: ArrayBuffer;
6+
}
7+
8+
export interface IBufferConversions {
9+
base64ToArrayBuffer(base64: string): ArrayBuffer;
10+
stringToBuffer(str: string): ArrayBuffer;
11+
}
12+
13+
/**
14+
* Returns a `Buffer` instance from the given data URI `uri`.
15+
*
16+
* @param {String} uri Data URI to turn into a Buffer instance
17+
*/
18+
export const makeDataUriToBuffer = (convert: IBufferConversions) => (uri: string | URL): ParsedDataURI => {
19+
uri = String(uri);
20+
21+
if (!/^data:/i.test(uri)) {
22+
throw new TypeError(
23+
'`uri` does not appear to be a Data URI (must begin with "data:")'
24+
);
25+
}
26+
27+
// strip newlines
28+
uri = uri.replace(/\r?\n/g, '');
29+
30+
// split the URI up into the "metadata" and the "data" portions
31+
const firstComma = uri.indexOf(',');
32+
if (firstComma === -1 || firstComma <= 4) {
33+
throw new TypeError('malformed data: URI');
34+
}
35+
36+
// remove the "data:" scheme and parse the metadata
37+
const meta = uri.substring(5, firstComma).split(';');
38+
39+
let charset = '';
40+
let base64 = false;
41+
const type = meta[0] || 'text/plain';
42+
let typeFull = type;
43+
for (let i = 1; i < meta.length; i++) {
44+
if (meta[i] === 'base64') {
45+
base64 = true;
46+
} else if (meta[i]) {
47+
typeFull += `;${meta[i]}`;
48+
if (meta[i].indexOf('charset=') === 0) {
49+
charset = meta[i].substring(8);
50+
}
51+
}
52+
}
53+
// defaults to US-ASCII only if type is not provided
54+
if (!meta[0] && !charset.length) {
55+
typeFull += ';charset=US-ASCII';
56+
charset = 'US-ASCII';
57+
}
58+
59+
// get the encoded data portion and decode URI-encoded chars
60+
const data = unescape(uri.substring(firstComma + 1));
61+
const buffer = base64 ? convert.base64ToArrayBuffer(data) : convert.stringToBuffer(data);
62+
63+
return {
64+
type,
65+
typeFull,
66+
charset,
67+
buffer,
68+
};
69+
}
+4-58
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
1-
export interface ParsedDataURI {
2-
type: string;
3-
typeFull: string;
4-
charset: string;
5-
buffer: ArrayBuffer;
6-
}
1+
import { makeDataUriToBuffer } from './common';
2+
3+
export type { ParsedDataURI } from './common';
74

85
function base64ToArrayBuffer(base64: string) {
96
const chars =
@@ -58,55 +55,4 @@ function stringToBuffer(str: string): ArrayBuffer {
5855
*
5956
* @param {String} uri Data URI to turn into a Buffer instance
6057
*/
61-
export function dataUriToBuffer(uri: string | URL): ParsedDataURI {
62-
uri = String(uri);
63-
64-
if (!/^data:/i.test(uri)) {
65-
throw new TypeError(
66-
'`uri` does not appear to be a Data URI (must begin with "data:")'
67-
);
68-
}
69-
70-
// strip newlines
71-
uri = uri.replace(/\r?\n/g, '');
72-
73-
// split the URI up into the "metadata" and the "data" portions
74-
const firstComma = uri.indexOf(',');
75-
if (firstComma === -1 || firstComma <= 4) {
76-
throw new TypeError('malformed data: URI');
77-
}
78-
79-
// remove the "data:" scheme and parse the metadata
80-
const meta = uri.substring(5, firstComma).split(';');
81-
82-
let charset = '';
83-
let base64 = false;
84-
const type = meta[0] || 'text/plain';
85-
let typeFull = type;
86-
for (let i = 1; i < meta.length; i++) {
87-
if (meta[i] === 'base64') {
88-
base64 = true;
89-
} else if (meta[i]) {
90-
typeFull += `;${meta[i]}`;
91-
if (meta[i].indexOf('charset=') === 0) {
92-
charset = meta[i].substring(8);
93-
}
94-
}
95-
}
96-
// defaults to US-ASCII only if type is not provided
97-
if (!meta[0] && !charset.length) {
98-
typeFull += ';charset=US-ASCII';
99-
charset = 'US-ASCII';
100-
}
101-
102-
// get the encoded data portion and decode URI-encoded chars
103-
const data = unescape(uri.substring(firstComma + 1));
104-
const buffer = base64 ? base64ToArrayBuffer(data) : stringToBuffer(data);
105-
106-
return {
107-
type,
108-
typeFull,
109-
charset,
110-
buffer,
111-
};
112-
}
58+
export const dataUriToBuffer = makeDataUriToBuffer({ stringToBuffer, base64ToArrayBuffer });
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { makeDataUriToBuffer } from './common';
2+
3+
export type { ParsedDataURI } from './common';
4+
5+
function nodeBuffertoArrayBuffer(nodeBuf: Buffer) {
6+
if (nodeBuf.byteLength === nodeBuf.buffer.byteLength) {
7+
return nodeBuf.buffer; // large strings may get their own memory allocation
8+
}
9+
const buffer = new ArrayBuffer(nodeBuf.byteLength);
10+
const view = new Uint8Array(buffer);
11+
view.set(nodeBuf);
12+
return buffer;
13+
}
14+
15+
function base64ToArrayBuffer(base64: string) {
16+
return nodeBuffertoArrayBuffer(Buffer.from(base64, 'base64'));
17+
}
18+
19+
function stringToBuffer(str: string): ArrayBuffer {
20+
return nodeBuffertoArrayBuffer(Buffer.from(str, 'ascii'));
21+
}
22+
23+
/**
24+
* Returns a `Buffer` instance from the given data URI `uri`.
25+
*
26+
* @param {String} uri Data URI to turn into a Buffer instance
27+
*/
28+
export const dataUriToBuffer = makeDataUriToBuffer({ stringToBuffer, base64ToArrayBuffer });

packages/data-uri-to-buffer/test/data-uri-to-buffer.test.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import assert from 'assert';
2-
import { dataUriToBuffer } from '../src';
2+
import { dataUriToBuffer as baseline } from '../src/index';
3+
import { dataUriToBuffer as node } from '../src/node';
34

4-
describe('data-uri-to-buffer', function () {
5+
describe('node', () => doTest(node));
6+
describe('baseline', () => doTest(baseline));
7+
8+
function doTest(dataUriToBuffer: typeof baseline) {
59
it('should decode bare-bones Data URIs', function () {
610
const uri = 'data:,Hello%2C%20World!';
711

@@ -187,4 +191,4 @@ describe('data-uri-to-buffer', function () {
187191
assert.equal('UTF-8', parsed.charset);
188192
assert.equal('abc', Buffer.from(parsed.buffer).toString());
189193
});
190-
});
194+
}

0 commit comments

Comments
 (0)