Files
suaveplan/packages/devtools/docs/pdf/src/content-stream/content-stream.test.ts
2026-05-27 23:29:53 +01:00

233 lines
7.8 KiB
TypeScript

import { describe, expect, it } from "@suaveplan/testing/runner";
import { PDF_CODES, PdfError } from "../error-codes/error-codes.js";
import { ContentStreamEncoder } from "./content-stream.js";
describe("ContentStreamEncoder — graphics state", () => {
it("save / restore graphics state balance", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder()
.saveGraphicsState()
.restoreGraphicsState()
.encode();
expect(out).toBe("q\nQ");
});
it("restoreGraphicsState without matching q raises", {
timeout: 5000,
}, () => {
expect(() =>
new ContentStreamEncoder().restoreGraphicsState()
).toThrowError(PdfError);
});
it("encode rejects unmatched save", { timeout: 5000 }, () => {
const enc = new ContentStreamEncoder().saveGraphicsState();
expect(() => enc.encode()).toThrowError(PdfError);
});
it("concatMatrix and setLineWidth emit numeric ops", {
timeout: 5000,
}, () => {
const out = new ContentStreamEncoder()
.concatMatrix(1, 0, 0, 1, 100, 200)
.setLineWidth(2.5)
.encode();
expect(out).toContain("1 0 0 1 100 200 cm");
expect(out).toContain("2.5000 w");
});
});
describe("ContentStreamEncoder — paths", () => {
it("rectangle + fill emits expected ops", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder()
.setFillColor({ r: 1, g: 0, b: 0 })
.rectangle(10, 10, 100, 50)
.fill()
.encode();
expect(out).toContain("1 0 0 rg");
expect(out).toContain("10 10 100 50 re");
expect(out).toContain("f");
});
it("moveTo / lineTo / closePath / stroke chain", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder()
.moveTo(0, 0)
.lineTo(10, 0)
.closePath()
.stroke()
.encode();
expect(out).toContain("0 0 m");
expect(out).toContain("10 0 l");
expect(out).toContain("h");
expect(out).toContain("S");
});
it("closeAndStroke and fillAndStroke emit the right ops", {
timeout: 5000,
}, () => {
const out = new ContentStreamEncoder()
.rectangle(0, 0, 5, 5)
.closeAndStroke()
.rectangle(0, 0, 5, 5)
.fillAndStroke()
.encode();
expect(out).toContain("s");
expect(out).toContain("B");
});
});
describe("ContentStreamEncoder — colour", () => {
it("setStrokeColor emits RG", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder()
.setStrokeColor({ r: 0, g: 0.5, b: 1 })
.encode();
expect(out).toContain("0 0.5000 1 RG");
});
it("out-of-range channel raises INVALID_RGB", { timeout: 5000 }, () => {
try {
new ContentStreamEncoder().setFillColor({
r: 1.5,
g: 0,
b: 0,
});
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(PdfError);
expect((err as PdfError).code).toBe(PDF_CODES.INVALID_RGB);
}
});
it("NaN channel raises INVALID_RGB on stroke", { timeout: 5000 }, () => {
try {
new ContentStreamEncoder().setStrokeColor({
r: Number.NaN,
g: 0,
b: 0,
});
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(PdfError);
expect((err as PdfError).code).toBe(PDF_CODES.INVALID_RGB);
}
});
});
describe("ContentStreamEncoder — text", () => {
it("text-showing operators round-trip", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder()
.beginText()
.setFont("F1", 12)
.moveTextCursor(100, 700)
.showText("Hello")
.endText()
.encode();
expect(out).toContain("BT");
expect(out).toContain("/F1 12 Tf");
expect(out).toContain("100 700 Td");
expect(out).toContain("(Hello) Tj");
expect(out).toContain("ET");
});
it("showText outside text mode raises", { timeout: 5000 }, () => {
try {
new ContentStreamEncoder().showText("oops");
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(PdfError);
expect((err as PdfError).code).toBe(PDF_CODES.RENDER_FAILED);
}
});
it("beginText twice without endText raises", { timeout: 5000 }, () => {
const enc = new ContentStreamEncoder().beginText();
expect(() => enc.beginText()).toThrowError(PdfError);
});
it("endText outside text mode raises", { timeout: 5000 }, () => {
expect(() => new ContentStreamEncoder().endText()).toThrowError(
PdfError
);
});
it("encode with unclosed text object raises", { timeout: 5000 }, () => {
const enc = new ContentStreamEncoder().beginText().setFont("F1", 12);
expect(() => enc.encode()).toThrowError(PdfError);
});
it("setTextMatrix / setLeading / nextLine emit the right operators", {
timeout: 5000,
}, () => {
const out = new ContentStreamEncoder()
.beginText()
.setFont("/F2", 10)
.setTextMatrix(1, 0, 0, 1, 50, 50)
.setLeading(14)
.showText("line1")
.nextLine()
.showText("line2")
.endText()
.encode();
expect(out).toContain("1 0 0 1 50 50 Tm");
expect(out).toContain("14 TL");
expect(out).toContain("T*");
expect(out).toContain("(line1) Tj");
expect(out).toContain("(line2) Tj");
expect(out).toContain("/F2 10 Tf");
});
it("escapes parens + backslashes in showText", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder()
.beginText()
.setFont("F1", 12)
.showText("(hi\\there)")
.endText()
.encode();
expect(out).toContain("(\\(hi\\\\there\\)) Tj");
});
});
describe("ContentStreamEncoder — XObjects", () => {
it("drawImage emits /name Do", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder().drawImage("Im0").encode();
expect(out).toContain("/Im0 Do");
});
it("drawImage inside text object raises", { timeout: 5000 }, () => {
const enc = new ContentStreamEncoder().beginText();
try {
enc.drawImage("Im0");
throw new Error("expected throw");
} catch (err) {
expect(err).toBeInstanceOf(PdfError);
expect((err as PdfError).code).toBe(PDF_CODES.RENDER_FAILED);
}
});
it("drawImage accepts an already-prefixed name", { timeout: 5000 }, () => {
const out = new ContentStreamEncoder().drawImage("/Im1").encode();
expect(out).toContain("/Im1 Do");
});
});
describe("ContentStreamEncoder — non-strict mode", () => {
it("non-strict allows out-of-order ops", { timeout: 5000 }, () => {
const enc = new ContentStreamEncoder({ strict: false });
// Sequence below would throw in strict mode.
enc.restoreGraphicsState();
enc.showText("free-form");
enc.endText();
const out = enc.encode();
expect(out).toContain("Q");
expect(out).toContain("(free-form) Tj");
expect(out).toContain("ET");
});
});
describe("ContentStreamEncoder — appendRaw + length", () => {
it("appendRaw injects a custom operator", { timeout: 5000 }, () => {
const enc = new ContentStreamEncoder().appendRaw("0 0 0 rg");
expect(enc.length).toBe(1);
expect(enc.encode()).toBe("0 0 0 rg");
});
});