Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [BREAKING CHANGE] Stop automatically uppercasing annotation option keys.
- Do not mutate options passed to `doc.annotate()` and its convenience methods (link, note, strike, lineAnnotation, rectAnnotation, ellipseAnnotation, textAnnotation, fileAnnotation)
- Persist font options when adding a new page. Fixes #1739
- Use `Uint8Array` instead of Node's `Buffer` internally

### [v0.19.1] - 2026-06-10

Expand Down
33 changes: 33 additions & 0 deletions lib/binary.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export const toBinaryString = (bytes) => {
return out;
};

export const fromBinaryString = (str) => {
const out = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
out[i] = str.charCodeAt(i) & 0xff;
}
return out;
};

export const fromBase64 = (b64) => {
const binary = atob(b64);
const out = new Uint8Array(binary.length);
Expand All @@ -21,6 +29,31 @@ export const fromBase64 = (b64) => {
return out;
};

export const concat = (chunks) => {
let length = 0;
for (const chunk of chunks) length += chunk.length;

const out = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
out.set(chunk, offset);
offset += chunk.length;
}
return out;
};

export const toUTF16BE = (str) => {
const out = new Uint8Array((str.length + 1) * 2);
out[0] = 0xfe;
out[1] = 0xff;
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);
out[i * 2 + 2] = code >> 8;
out[i * 2 + 3] = code & 0xff;
}
return out;
};

export const readUInt16BE = (bytes, offset = 0) =>
((bytes[offset] << 8) | bytes[offset + 1]) >>> 0;

Expand Down
3 changes: 2 additions & 1 deletion lib/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import LineWrapper from './line_wrapper';
import SubsetMixin from './mixins/subsets';
import TableMixin from './mixins/table';
import MetadataMixin from './mixins/metadata';
import { fromBinaryString } from './binary';

class PDFDocument extends stream.Readable {
constructor(options = {}) {
Expand Down Expand Up @@ -265,7 +266,7 @@ class PDFDocument extends stream.Readable {

_write(data) {
if (!(data instanceof Uint8Array)) {
data = Buffer.from(data + '\n', 'binary');
data = fromBinaryString(data + '\n');
}

this.push(data);
Expand Down
2 changes: 1 addition & 1 deletion lib/font/embedded.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ class EmbeddedFont extends PDFFont {

if (this.document.subset && this.document.subset === 1) {
const maxCID = this.widths.length - 1;
const cidSetBuffer = Buffer.alloc(Math.ceil((maxCID + 1) / 8), 0);
const cidSetBuffer = new Uint8Array(Math.ceil((maxCID + 1) / 8));
for (let cid = 0; cid <= maxCID; cid++) {
if (this.widths[cid] != null) {
cidSetBuffer[Math.floor(cid / 8)] |= 0x80 >> cid % 8;
Expand Down
4 changes: 2 additions & 2 deletions lib/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import PNG from './image/png';
class PDFImage {
static open(src, label) {
let data;
if (Buffer.isBuffer(src)) {
if (src instanceof Uint8Array) {
data = src;
} else if (src instanceof ArrayBuffer) {
data = Buffer.from(new Uint8Array(src));
data = new Uint8Array(src);
} else {
const match = /^data:.+?;base64,(.*)$/.exec(src);
if (match) {
Expand Down
8 changes: 4 additions & 4 deletions lib/image/png.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class PNGImage {
} else {
// embed the color palette in the PDF as an object stream
const palette = document.ref();
palette.end(Buffer.from(image.palette));
palette.end(new Uint8Array(image.palette));

// build the color space array for the image
obj.data['ColorSpace'] = [
Expand Down Expand Up @@ -126,8 +126,8 @@ class PNGImage {
let a, p;
const colorCount = this.image.colors;
const pixelCount = this.width * this.height;
const imgData = Buffer.alloc(pixelCount * colorCount);
const alphaChannel = Buffer.alloc(pixelCount);
const imgData = new Uint8Array(pixelCount * colorCount);
const alphaChannel = new Uint8Array(pixelCount);

let i = (p = a = 0);
const len = pixels.length;
Expand All @@ -152,7 +152,7 @@ class PNGImage {
const transparency = this.image.transparency.indexed;
const isInterlaced = this.image.interlaceMethod === 1;
return this.image.decodePixels((pixels) => {
const alphaChannel = Buffer.alloc(this.width * this.height);
const alphaChannel = new Uint8Array(this.width * this.height);

let i = 0;
for (let j = 0, end = pixels.length; j < end; j++) {
Expand Down
9 changes: 5 additions & 4 deletions lib/mixins/attachments.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import fs from '#fs';
import { md5Hex } from '../crypto/md5';
import { escapeName } from '../object.js';
import { fromBase64 } from '../binary';

export default {
/**
* Embed contents of `src` in PDF
* @param {Buffer | ArrayBuffer | string} src input Buffer, ArrayBuffer, base64 encoded string or path to file
* @param {Buffer | Uint8Array | ArrayBuffer | string} src input Buffer, Uint8Array, ArrayBuffer, base64 encoded string or path to file
* @param {object} options
* * options.name: filename to be shown in PDF, will use `src` if none set
* * options.type: filetype to be shown in PDF
Expand All @@ -29,17 +30,17 @@ export default {
if (!src) {
throw new Error('No src specified');
}
if (Buffer.isBuffer(src)) {
if (src instanceof Uint8Array) {
data = src;
} else if (src instanceof ArrayBuffer) {
data = Buffer.from(new Uint8Array(src));
data = new Uint8Array(src);
} else {
const match = /^data:(.*?);base64,(.*)$/.exec(src);
if (match) {
if (match[1]) {
refBody.Subtype = escapeName(match[1]);
}
data = Buffer.from(match[2], 'base64');
data = fromBase64(match[2]);
} else {
data = fs.readFileSync(src);
if (!data) {
Expand Down
2 changes: 1 addition & 1 deletion lib/mixins/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default {
Subtype: 'XML',
});
this.metadataRef.compress = false;
this.metadataRef.write(Buffer.from(this.metadata.getXML(), 'utf-8'));
this.metadataRef.write(new TextEncoder().encode(this.metadata.getXML()));
this.metadataRef.end();
this._root.data.Metadata = this.metadataRef;
}
Expand Down
38 changes: 12 additions & 26 deletions lib/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ PDFObject - converts JavaScript types into their corresponding PDF types.
By Devon Govett
*/

import { bytesToHex } from '@noble/hashes/utils';
import PDFAbstractReference from './abstract_reference';
import PDFTree from './tree';
import SpotColor from './spotcolor';
import { fromBinaryString, toBinaryString, toUTF16BE } from './binary';

const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length);

Expand Down Expand Up @@ -58,22 +60,6 @@ export const escapeName = function (name) {
return escapedName;
};

// Convert little endian UTF-16 to big endian
const swapBytes = function (buff) {
const l = buff.length;
if (l & 0x01) {
throw new Error('Buffer length must be even');
} else {
for (let i = 0, end = l - 1; i < end; i += 2) {
const a = buff[i];
buff[i] = buff[i + 1];
buff[i + 1] = a;
}
}

return buff;
};

class PDFObject {
static convert(object, encryptFn = null) {
// String literals are converted to the PDF name type
Expand All @@ -93,28 +79,28 @@ class PDFObject {
}

// If so, encode it as big endian UTF-16
let stringBuffer;
let stringBytes;
if (isUnicode) {
stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le'));
stringBytes = toUTF16BE(string.valueOf());
} else {
stringBuffer = Buffer.from(string.valueOf(), 'ascii');
stringBytes = fromBinaryString(string.valueOf());
}

// Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(stringBuffer).toString('binary');
} else {
string = stringBuffer.toString('binary');
stringBytes = encryptFn(stringBytes);
}

string = toBinaryString(stringBytes);

// Escape characters as required by the spec
string = string.replace(escapableRe, (c) => escapable[c]);

return `(${string})`;

// Buffers are converted to PDF hex strings
} else if (Buffer.isBuffer(object)) {
return `<${object.toString('hex')}>`;
// Byte arrays are converted to PDF hex strings
} else if (object instanceof Uint8Array) {
return `<${bytesToHex(object)}>`;
} else if (
object instanceof PDFAbstractReference ||
object instanceof PDFTree ||
Expand All @@ -133,7 +119,7 @@ class PDFObject {

// Encrypt the string when necessary
if (encryptFn) {
string = encryptFn(Buffer.from(string, 'ascii')).toString('binary');
string = toBinaryString(encryptFn(fromBinaryString(string)));

// Escape characters as required by the spec
string = string.replace(escapableRe, (c) => escapable[c]);
Expand Down
5 changes: 3 additions & 2 deletions lib/reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ By Devon Govett
import zlib from '#zlib';
import PDFAbstractReference from './abstract_reference';
import PDFObject from './object';
import { concat, fromBinaryString } from './binary';

class PDFReference extends PDFAbstractReference {
constructor(document, id, data = {}) {
Expand All @@ -21,7 +22,7 @@ class PDFReference extends PDFAbstractReference {

write(chunk) {
if (!(chunk instanceof Uint8Array)) {
chunk = Buffer.from(chunk + '\n', 'binary');
chunk = fromBinaryString(chunk + '\n');
}

this.uncompressedLength += chunk.length;
Expand Down Expand Up @@ -50,7 +51,7 @@ class PDFReference extends PDFAbstractReference {
: null;

if (this.buffer.length) {
this.buffer = Buffer.concat(this.buffer);
this.buffer = concat(this.buffer);
if (this.compress) {
this.buffer = zlib.deflateSync(this.buffer);
}
Expand Down
22 changes: 11 additions & 11 deletions lib/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PDFSecurity {
infoStr += `${key}: ${info[key].valueOf()}\n`;
}

return Buffer.from(md5Hash(infoStr));
return md5Hash(infoStr);
}

static generateRandomWordArray(bytes) {
Expand Down Expand Up @@ -147,8 +147,8 @@ class PDFSecurity {
encDict.StrF = 'StdCF';
}
encDict.R = r;
encDict.O = Buffer.from(ownerPasswordEntry);
encDict.U = Buffer.from(userPasswordEntry);
encDict.O = ownerPasswordEntry;
encDict.U = userPasswordEntry;
encDict.P = permissions;
}

Expand Down Expand Up @@ -204,12 +204,12 @@ class PDFSecurity {
encDict.StmF = 'StdCF';
encDict.StrF = 'StdCF';
encDict.R = 5;
encDict.O = Buffer.from(ownerPasswordEntry);
encDict.OE = Buffer.from(ownerEncryptionKeyEntry);
encDict.U = Buffer.from(userPasswordEntry);
encDict.UE = Buffer.from(userEncryptionKeyEntry);
encDict.O = ownerPasswordEntry;
encDict.OE = ownerEncryptionKeyEntry;
encDict.U = userPasswordEntry;
encDict.UE = userEncryptionKeyEntry;
encDict.P = permissions;
encDict.Perms = Buffer.from(permsEntry);
encDict.Perms = permsEntry;
}

getEncryptFn(obj, gen) {
Expand All @@ -230,7 +230,7 @@ class PDFSecurity {
let key = md5Hash(digest);
const keyLen = Math.min(16, this.keyBits / 8 + 5);
key = key.slice(0, keyLen);
return (buffer) => Buffer.from(rc4(new Uint8Array(buffer), key));
return (buffer) => rc4(buffer, key);
}

let key;
Expand All @@ -245,8 +245,8 @@ class PDFSecurity {
const iv = PDFSecurity.generateRandomWordArray(16);

return (buffer) => {
const encrypted = aesCbcEncrypt(new Uint8Array(buffer), key, iv, true);
return Buffer.from(concatBytes(iv, encrypted));
const encrypted = aesCbcEncrypt(buffer, key, iv, true);
return concatBytes(iv, encrypted);
};
}

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function logData(doc) {
const loggedData = [];
const originalMethod = doc._write;
doc._write = function (data) {
loggedData.push(data);
loggedData.push(data instanceof Uint8Array ? Buffer.from(data) : data);
originalMethod.call(this, data);
};
return loggedData;
Expand Down
Loading
Loading