233 lines
7.8 KiB
TypeScript
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");
|
|
});
|
|
});
|