Skip to content

Commit 1cab448

Browse files
authored
perf: reduce allocation churn in operator serialization (#31)
1 parent c625853 commit 1cab448

2 files changed

Lines changed: 35 additions & 34 deletions

File tree

src/content/operators.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
* This module provides type-safe creation and serialization of operators.
1616
*/
1717

18-
import { concatBytes } from "#src/helpers/buffer";
1918
import { formatPdfNumber } from "#src/helpers/format";
2019
import { ByteWriter } from "#src/io/byte-writer";
2120
import type { PdfArray } from "#src/objects/pdf-array";
@@ -26,7 +25,6 @@ import type { PdfString } from "#src/objects/pdf-string";
2625
/** Valid operand types */
2726
export type Operand = number | string | PdfName | PdfString | PdfArray | PdfDict;
2827

29-
const encoder = new TextEncoder();
3028
const SPACE = 0x20;
3129

3230
/** All PDF content stream operator names */
@@ -145,24 +143,27 @@ export class Operator {
145143
}
146144

147145
/**
148-
* Serialize to bytes for content stream output.
149-
* Format: "operand1 operand2 ... operator"
146+
* Write operator bytes directly into a shared ByteWriter.
147+
* Avoids intermediate allocations compared to toBytes().
150148
*/
151-
toBytes(): Uint8Array {
152-
if (this.operands.length === 0) {
153-
return encoder.encode(this.op);
154-
}
155-
156-
const parts: Uint8Array[] = [];
157-
149+
writeTo(writer: ByteWriter): void {
158150
for (const operand of this.operands) {
159-
parts.push(serializeOperand(operand));
160-
parts.push(new Uint8Array([SPACE]));
151+
writeOperand(writer, operand);
152+
writer.writeByte(SPACE);
161153
}
162154

163-
parts.push(encoder.encode(this.op));
155+
writer.writeAscii(this.op);
156+
}
157+
158+
/**
159+
* Serialize to bytes for content stream output.
160+
* Format: "operand1 operand2 ... operator"
161+
*/
162+
toBytes(): Uint8Array {
163+
const writer = new ByteWriter(undefined, { initialSize: 64 });
164+
this.writeTo(writer);
164165

165-
return concatBytes(parts);
166+
return writer.toBytes();
166167
}
167168

168169
/**
@@ -174,29 +175,28 @@ export class Operator {
174175
}
175176

176177
/**
177-
* Get byte length when serialized (for pre-allocation).
178+
* Get byte length when serialized.
179+
*
180+
* Should be avoided in performance-critical paths, use {@link writeTo} instead.
178181
*/
179182
byteLength(): number {
180183
return this.toBytes().length;
181184
}
182185
}
183186

184-
/**
185-
* Serialize an operand to bytes.
186-
*/
187-
function serializeOperand(operand: Operand): Uint8Array {
187+
/** Write an operand directly into a ByteWriter. */
188+
function writeOperand(writer: ByteWriter, operand: Operand): void {
188189
if (typeof operand === "number") {
189-
return encoder.encode(formatPdfNumber(operand));
190+
writer.writeAscii(formatPdfNumber(operand));
191+
return;
190192
}
191193

192194
if (typeof operand === "string") {
193195
// Assume already formatted (e.g., "/FontName")
194-
return encoder.encode(operand);
196+
writer.writeAscii(operand);
197+
return;
195198
}
196199

197-
// PdfName, PdfString, PdfArray, PdfDict all have toBytes()
198-
const writer = new ByteWriter();
200+
// PdfName, PdfString, PdfArray, PdfDict all have toBytes(writer)
199201
operand.toBytes(writer);
200-
201-
return writer.toBytes();
202202
}

src/drawing/serialize.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,30 @@
33
*/
44

55
import type { Operator } from "#src/content/operators";
6-
import { concatBytes } from "#src/helpers/buffer";
6+
import { ByteWriter } from "#src/io/byte-writer";
7+
8+
const NEWLINE = 0x0a;
79

810
/**
911
* Serialize operators to bytes for content streams.
1012
*
11-
* Uses Operator.toBytes() directly to avoid UTF-8 round-trip corruption
12-
* of non-ASCII bytes in PdfString operands (e.g., WinAnsi-encoded text).
13+
* Uses Operator.writeTo() to write directly into a shared ByteWriter,
14+
* avoiding per-operator intermediate allocations.
1315
*/
1416
export function serializeOperators(ops: Operator[]): Uint8Array {
1517
if (ops.length === 0) {
1618
return new Uint8Array(0);
1719
}
1820

19-
const newline = new Uint8Array([0x0a]);
20-
const parts: Uint8Array[] = [];
21+
const writer = new ByteWriter(undefined, { initialSize: ops.length * 24 });
2122

2223
for (let i = 0; i < ops.length; i++) {
2324
if (i > 0) {
24-
parts.push(newline);
25+
writer.writeByte(NEWLINE);
2526
}
2627

27-
parts.push(ops[i].toBytes());
28+
ops[i].writeTo(writer);
2829
}
2930

30-
return concatBytes(parts);
31+
return writer.toBytes();
3132
}

0 commit comments

Comments
 (0)