diff --git a/.gitignore b/.gitignore
index c7abc6a..0d003c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,6 +50,7 @@ debug/
# Benchmark fixtures (downloaded at runtime)
fixtures/benchmarks/
+fixtures/private/
# Temporary files
tmp/
diff --git a/content/docs/advanced/low-level-drawing.mdx b/content/docs/advanced/low-level-drawing.mdx
index 5271d11..6c9928c 100644
--- a/content/docs/advanced/low-level-drawing.mdx
+++ b/content/docs/advanced/low-level-drawing.mdx
@@ -25,9 +25,8 @@ page.drawOperators([
```
- The low-level API requires understanding of PDF content stream structure.
- Invalid operator sequences may produce corrupted PDFs. Use the high-level
- methods when they're sufficient.
+ The low-level API requires understanding of PDF content stream structure. Invalid operator
+ sequences may produce corrupted PDFs. Use the high-level methods when they're sufficient.
---
@@ -36,15 +35,15 @@ page.drawOperators([
The high-level methods (`drawRectangle`, `drawText`, etc.) cover most needs. Reach for the low-level API when you need:
-| Feature | Low-Level Approach |
-| --- | --- |
-| Matrix transforms | `ops.concatMatrix()` for arbitrary rotation/scale/skew |
-| Gradients | `createAxialShading()` or `createRadialShading()` |
-| Repeating patterns | `createTilingPattern()` or `createImagePattern()` |
-| Blend modes | `createExtGState({ blendMode: "Multiply" })` |
-| Clipping regions | `ops.clip()` with `ops.endPath()` |
-| Reusable graphics | `createFormXObject()` for stamps/watermarks |
-| Fine-grained control | Direct operator sequences |
+| Feature | Low-Level Approach |
+| -------------------- | ------------------------------------------------------ |
+| Matrix transforms | `ops.concatMatrix()` for arbitrary rotation/scale/skew |
+| Gradients | `createAxialShading()` or `createRadialShading()` |
+| Repeating patterns | `createTilingPattern()` or `createImagePattern()` |
+| Blend modes | `createExtGState({ blendMode: "Multiply" })` |
+| Clipping regions | `ops.clip()` with `ops.endPath()` |
+| Reusable graphics | `createFormXObject()` for stamps/watermarks |
+| Fine-grained control | Direct operator sequences |
---
@@ -59,80 +58,80 @@ import { ops } from "@libpdf/core";
### Graphics State
```typescript
-ops.pushGraphicsState() // Save current state (q)
-ops.popGraphicsState() // Restore saved state (Q)
-ops.setGraphicsState(name) // Apply ExtGState resource (gs)
-ops.concatMatrix(a, b, c, d, e, f) // Transform CTM (cm)
+ops.pushGraphicsState(); // Save current state (q)
+ops.popGraphicsState(); // Restore saved state (Q)
+ops.setGraphicsState(name); // Apply ExtGState resource (gs)
+ops.concatMatrix(a, b, c, d, e, f); // Transform CTM (cm)
```
### Path Construction
```typescript
-ops.moveTo(x, y) // Begin subpath (m)
-ops.lineTo(x, y) // Line to point (l)
-ops.curveTo(x1, y1, x2, y2, x3, y3) // Cubic bezier (c)
-ops.rectangle(x, y, w, h) // Rectangle shorthand (re)
-ops.closePath() // Close subpath (h)
+ops.moveTo(x, y); // Begin subpath (m)
+ops.lineTo(x, y); // Line to point (l)
+ops.curveTo(x1, y1, x2, y2, x3, y3); // Cubic bezier (c)
+ops.rectangle(x, y, w, h); // Rectangle shorthand (re)
+ops.closePath(); // Close subpath (h)
```
### Path Painting
```typescript
-ops.stroke() // Stroke path (S)
-ops.fill() // Fill path, non-zero winding (f)
-ops.fillEvenOdd() // Fill path, even-odd rule (f*)
-ops.fillAndStroke() // Fill then stroke (B)
-ops.endPath() // Discard path without painting (n)
+ops.stroke(); // Stroke path (S)
+ops.fill(); // Fill path, non-zero winding (f)
+ops.fillEvenOdd(); // Fill path, even-odd rule (f*)
+ops.fillAndStroke(); // Fill then stroke (B)
+ops.endPath(); // Discard path without painting (n)
```
### Clipping
```typescript
-ops.clip() // Set clip region, non-zero (W)
-ops.clipEvenOdd() // Set clip region, even-odd (W*)
+ops.clip(); // Set clip region, non-zero (W)
+ops.clipEvenOdd(); // Set clip region, even-odd (W*)
```
### Color
```typescript
-ops.setStrokingGray(g) // Stroke grayscale (G)
-ops.setNonStrokingGray(g) // Fill grayscale (g)
-ops.setStrokingRGB(r, g, b) // Stroke RGB (RG)
-ops.setNonStrokingRGB(r, g, b) // Fill RGB (rg)
-ops.setStrokingCMYK(c, m, y, k) // Stroke CMYK (K)
-ops.setNonStrokingCMYK(c, m, y, k) // Fill CMYK (k)
-ops.setStrokingColorSpace(cs) // Set stroke color space (CS)
-ops.setNonStrokingColorSpace(cs) // Set fill color space (cs)
-ops.setStrokingColorN(name) // Set stroke pattern (SCN)
-ops.setNonStrokingColorN(name) // Set fill pattern (scn)
+ops.setStrokingGray(g); // Stroke grayscale (G)
+ops.setNonStrokingGray(g); // Fill grayscale (g)
+ops.setStrokingRGB(r, g, b); // Stroke RGB (RG)
+ops.setNonStrokingRGB(r, g, b); // Fill RGB (rg)
+ops.setStrokingCMYK(c, m, y, k); // Stroke CMYK (K)
+ops.setNonStrokingCMYK(c, m, y, k); // Fill CMYK (k)
+ops.setStrokingColorSpace(cs); // Set stroke color space (CS)
+ops.setNonStrokingColorSpace(cs); // Set fill color space (cs)
+ops.setStrokingColorN(name); // Set stroke pattern (SCN)
+ops.setNonStrokingColorN(name); // Set fill pattern (scn)
```
### Line Style
```typescript
-ops.setLineWidth(w) // Line width (w)
-ops.setLineCap(cap) // 0=butt, 1=round, 2=square (J)
-ops.setLineJoin(join) // 0=miter, 1=round, 2=bevel (j)
-ops.setMiterLimit(limit) // Miter limit ratio (M)
-ops.setDashPattern(array, phase) // Dash pattern (d)
+ops.setLineWidth(w); // Line width (w)
+ops.setLineCap(cap); // 0=butt, 1=round, 2=square (J)
+ops.setLineJoin(join); // 0=miter, 1=round, 2=bevel (j)
+ops.setMiterLimit(limit); // Miter limit ratio (M)
+ops.setDashPattern(array, phase); // Dash pattern (d)
```
### Text
```typescript
-ops.beginText() // Begin text object (BT)
-ops.endText() // End text object (ET)
-ops.setFont(name, size) // Set font (Tf)
-ops.moveText(tx, ty) // Position text (Td)
-ops.setTextMatrix(a, b, c, d, e, f) // Text matrix (Tm)
-ops.showText(string) // Show text (Tj)
+ops.beginText(); // Begin text object (BT)
+ops.endText(); // End text object (ET)
+ops.setFont(name, size); // Set font (Tf)
+ops.moveText(tx, ty); // Position text (Td)
+ops.setTextMatrix(a, b, c, d, e, f); // Text matrix (Tm)
+ops.showText(string); // Show text (Tj)
```
### XObjects and Shading
```typescript
-ops.paintXObject(name) // Draw XObject (Do)
-ops.paintShading(name) // Paint shading (sh)
+ops.paintXObject(name); // Draw XObject (Do)
+ops.paintShading(name); // Paint shading (sh)
```
---
@@ -146,7 +145,7 @@ import { Matrix, ops } from "@libpdf/core";
const matrix = Matrix.identity()
.translate(200, 300)
- .rotate(45) // degrees
+ .rotate(45) // degrees
.scale(2, 1.5);
page.drawOperators([
@@ -162,14 +161,14 @@ Or use raw matrix components:
```typescript
// Translation: move 100 points right, 200 points up
-ops.concatMatrix(1, 0, 0, 1, 100, 200)
+ops.concatMatrix(1, 0, 0, 1, 100, 200);
// Scale: 2x horizontal, 0.5x vertical
-ops.concatMatrix(2, 0, 0, 0.5, 0, 0)
+ops.concatMatrix(2, 0, 0, 0.5, 0, 0);
// Rotation: 45 degrees around origin
-const angle = 45 * Math.PI / 180;
-ops.concatMatrix(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0)
+const angle = (45 * Math.PI) / 180;
+ops.concatMatrix(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), 0, 0);
```
---
@@ -183,7 +182,7 @@ Create linear or radial gradients with color stops:
```typescript
// CSS-style: angle + length
const gradient = pdf.createLinearGradient({
- angle: 90, // 0=up, 90=right, 180=down, 270=left
+ angle: 90, // 0=up, 90=right, 180=down, 270=left
length: 200,
stops: [
{ offset: 0, color: rgb(1, 0, 0) },
@@ -194,7 +193,7 @@ const gradient = pdf.createLinearGradient({
// Or explicit coordinates
const axial = pdf.createAxialShading({
- coords: [0, 0, 200, 0], // x0, y0, x1, y1
+ coords: [0, 0, 200, 0], // x0, y0, x1, y1
stops: [
{ offset: 0, color: rgb(0, 0, 1) },
{ offset: 1, color: rgb(1, 0, 1) },
@@ -206,7 +205,7 @@ const axial = pdf.createAxialShading({
```typescript
const radial = pdf.createRadialShading({
- coords: [100, 100, 0, 100, 100, 80], // x0, y0, r0, x1, y1, r1
+ coords: [100, 100, 0, 100, 100, 80], // x0, y0, r0, x1, y1, r1
stops: [
{ offset: 0, color: rgb(1, 1, 1) },
{ offset: 1, color: rgb(0, 0, 0) },
@@ -234,9 +233,7 @@ page.drawOperators([
// Or wrap in a pattern for PathBuilder
const pattern = pdf.createShadingPattern({ shading: gradient });
-page.drawPath()
- .rectangle(50, 200, 200, 100)
- .fill({ pattern });
+page.drawPath().rectangle(50, 200, 200, 100).fill({ pattern });
```
---
@@ -282,9 +279,7 @@ const pattern = pdf.createImagePattern({
height: 50,
});
-page.drawPath()
- .circle(200, 400, 80)
- .fill({ pattern });
+page.drawPath().circle(200, 400, 80).fill({ pattern });
```
### Gradient Pattern
@@ -387,23 +382,23 @@ Restrict drawing to a region:
```typescript
page.drawOperators([
ops.pushGraphicsState(),
-
+
// Define clip region (circle)
ops.moveTo(200, 300),
// ... circle path using bezier curves
ops.clip(),
- ops.endPath(), // Required after clip
-
+ ops.endPath(), // Required after clip
+
// Everything here is clipped to the circle
ops.paintShading(gradientName),
-
- ops.popGraphicsState(), // Clipping is restored
+
+ ops.popGraphicsState(), // Clipping is restored
]);
```
- Always follow `ops.clip()` with a path-painting operator. Use `ops.endPath()`
- to discard the path, or `ops.fill()` to both clip and fill.
+ Always follow `ops.clip()` with a path-painting operator. Use `ops.endPath()` to discard the path,
+ or `ops.fill()` to both clip and fill.
---
@@ -444,7 +439,7 @@ const page = pdf.addPage();
// Create button gradient
const gradient = pdf.createLinearGradient({
- angle: 180, // top to bottom
+ angle: 180, // top to bottom
length: 40,
stops: [
{ offset: 0, color: rgb(0.4, 0.6, 1) },
@@ -475,7 +470,8 @@ page.drawOperators([
]);
// Rounded rectangle path
-page.drawPath()
+page
+ .drawPath()
.moveTo(110, 700)
.lineTo(250, 700)
.curveTo(255, 700, 260, 705, 260, 710)
diff --git a/content/docs/api/pdf-page.mdx b/content/docs/api/pdf-page.mdx
index 1b30d01..8d73ca4 100644
--- a/content/docs/api/pdf-page.mdx
+++ b/content/docs/api/pdf-page.mdx
@@ -570,9 +570,9 @@ For advanced graphics operations, PDFPage provides methods to emit raw operators
Emit raw PDF operators to the page content stream.
-| Param | Type | Description |
-| ----------- | ------------ | --------------------------- |
-| `operators` | `Operator[]` | Array of operators to emit |
+| Param | Type | Description |
+| ----------- | ------------ | -------------------------- |
+| `operators` | `Operator[]` | Array of operators to emit |
```typescript
import { ops } from "@libpdf/core";
@@ -588,8 +588,8 @@ page.drawOperators([
```
- The caller is responsible for valid operator sequences. Invalid sequences
- may produce corrupted PDFs.
+ The caller is responsible for valid operator sequences. Invalid sequences may produce corrupted
+ PDFs.
---
@@ -598,8 +598,8 @@ page.drawOperators([
Register a font resource and return its operator name.
-| Param | Type | Description |
-| ------ | ------------------------------ | ------------------ |
+| Param | Type | Description |
+| ------ | ------------------------------------ | ---------------- |
| `font` | `EmbeddedFont \| Standard14FontName` | Font to register |
**Returns**: `string` - Resource name (e.g., `"F0"`)
@@ -623,9 +623,9 @@ page.drawOperators([
Register an image resource and return its operator name.
-| Param | Type | Description |
-| ------- | ---------- | ------------------ |
-| `image` | `PDFImage` | Embedded image |
+| Param | Type | Description |
+| ------- | ---------- | -------------- |
+| `image` | `PDFImage` | Embedded image |
**Returns**: `string` - Resource name (e.g., `"Im0"`)
@@ -647,8 +647,8 @@ page.drawOperators([
Register a shading (gradient) resource and return its operator name.
-| Param | Type | Description |
-| --------- | ------------ | ------------ |
+| Param | Type | Description |
+| --------- | ------------ | ---------------- |
| `shading` | `PDFShading` | Shading resource |
**Returns**: `string` - Resource name (e.g., `"Sh0"`)
@@ -699,8 +699,8 @@ page.drawOperators([
Register an extended graphics state and return its operator name.
-| Param | Type | Description |
-| ------- | ------------- | -------------- |
+| Param | Type | Description |
+| ------- | -------------- | -------------- |
| `state` | `PDFExtGState` | Graphics state |
**Returns**: `string` - Resource name (e.g., `"GS0"`)
@@ -728,8 +728,8 @@ page.drawOperators([
Register a Form XObject or embedded page and return its operator name.
-| Param | Type | Description |
-| --------- | --------------------------------- | ---------------- |
+| Param | Type | Description |
+| --------- | ----------------------------------- | ------------------- |
| `xobject` | `PDFFormXObject \| PDFEmbeddedPage` | XObject to register |
**Returns**: `string` - Resource name (e.g., `"Fm0"`)
diff --git a/content/docs/api/pdf.mdx b/content/docs/api/pdf.mdx
index 87248e5..d212c14 100644
--- a/content/docs/api/pdf.mdx
+++ b/content/docs/api/pdf.mdx
@@ -653,11 +653,11 @@ These methods create PDF resources for advanced drawing operations. See [Low-Lev
Create a linear gradient using CSS-style angle and length.
-| Param | Type | Default | Description |
-| ---------------- | ------------- | -------- | ----------------------------------------- |
-| `options.angle` | `number` | required | Angle in degrees (0=up, 90=right) |
-| `options.length` | `number` | required | Gradient length in points |
-| `options.stops` | `ColorStop[]` | required | Color stops with offset (0-1) and color |
+| Param | Type | Default | Description |
+| ---------------- | ------------- | -------- | --------------------------------------- |
+| `options.angle` | `number` | required | Angle in degrees (0=up, 90=right) |
+| `options.length` | `number` | required | Gradient length in points |
+| `options.stops` | `ColorStop[]` | required | Color stops with offset (0-1) and color |
**Returns**: `PDFShading`
@@ -681,10 +681,10 @@ page.drawRectangle({ x: 50, y: 50, width: 200, height: 100, pattern });
Create an axial (linear) shading with explicit coordinates.
-| Param | Type | Default | Description |
-| --------------- | ------------------------------------ | -------- | ----------------------------- |
-| `options.coords` | `[x0, y0, x1, y1]` | required | Start and end points |
-| `options.stops` | `ColorStop[]` | required | Color stops |
+| Param | Type | Default | Description |
+| ---------------- | ------------------ | -------- | -------------------- |
+| `options.coords` | `[x0, y0, x1, y1]` | required | Start and end points |
+| `options.stops` | `ColorStop[]` | required | Color stops |
**Returns**: `PDFShading`
@@ -705,10 +705,10 @@ const gradient = pdf.createAxialShading({
Create a radial shading with explicit coordinates.
-| Param | Type | Default | Description |
-| ---------------- | ----------------------------------- | -------- | ------------------------------------ |
-| `options.coords` | `[x0, y0, r0, x1, y1, r1]` | required | Center and radius for both circles |
-| `options.stops` | `ColorStop[]` | required | Color stops |
+| Param | Type | Default | Description |
+| ---------------- | -------------------------- | -------- | ---------------------------------- |
+| `options.coords` | `[x0, y0, r0, x1, y1, r1]` | required | Center and radius for both circles |
+| `options.stops` | `ColorStop[]` | required | Color stops |
**Returns**: `PDFShading`
@@ -728,11 +728,11 @@ const radial = pdf.createRadialShading({
Create a repeating tiling pattern.
-| Param | Type | Default | Description |
-| ------------------ | ------------ | -------- | -------------------------------- |
-| `options.bbox` | `BBox` | required | Pattern cell bounding box |
-| `options.xStep` | `number` | required | Horizontal spacing between tiles |
-| `options.yStep` | `number` | required | Vertical spacing between tiles |
+| Param | Type | Default | Description |
+| ------------------- | ------------ | -------- | --------------------------------- |
+| `options.bbox` | `BBox` | required | Pattern cell bounding box |
+| `options.xStep` | `number` | required | Horizontal spacing between tiles |
+| `options.yStep` | `number` | required | Vertical spacing between tiles |
| `options.operators` | `Operator[]` | required | Content operators for the pattern |
**Returns**: `PDFTilingPattern`
@@ -742,11 +742,7 @@ const pattern = pdf.createTilingPattern({
bbox: { x: 0, y: 0, width: 10, height: 10 },
xStep: 10,
yStep: 10,
- operators: [
- ops.setNonStrokingGray(0.8),
- ops.rectangle(0, 0, 5, 5),
- ops.fill(),
- ],
+ operators: [ops.setNonStrokingGray(0.8), ops.rectangle(0, 0, 5, 5), ops.fill()],
});
```
@@ -781,10 +777,10 @@ page.drawCircle({ x: 200, y: 400, radius: 80, pattern });
Wrap a shading (gradient) as a pattern for use with drawing methods.
-| Param | Type | Default | Description |
-| ----------------- | ------------ | -------- | --------------------- |
-| `options.shading` | `PDFShading` | required | Shading to wrap |
-| `[options.matrix]` | `PatternMatrix` | | Transform matrix |
+| Param | Type | Default | Description |
+| ------------------ | --------------- | -------- | ---------------- |
+| `options.shading` | `PDFShading` | required | Shading to wrap |
+| `[options.matrix]` | `PatternMatrix` | | Transform matrix |
**Returns**: `PDFShadingPattern`
@@ -803,11 +799,11 @@ page.drawPath()
Create an extended graphics state for opacity and blend modes.
-| Param | Type | Default | Description |
-| ------------------------ | ----------- | ------- | ---------------- |
-| `[options.fillOpacity]` | `number` | | Fill opacity 0-1 |
+| Param | Type | Default | Description |
+| ------------------------- | ----------- | ------- | ------------------ |
+| `[options.fillOpacity]` | `number` | | Fill opacity 0-1 |
| `[options.strokeOpacity]` | `number` | | Stroke opacity 0-1 |
-| `[options.blendMode]` | `BlendMode` | | Blend mode |
+| `[options.blendMode]` | `BlendMode` | | Blend mode |
**Returns**: `PDFExtGState`
@@ -827,9 +823,9 @@ const gsName = page.registerExtGState(gs);
Create a reusable Form XObject (content block).
-| Param | Type | Default | Description |
-| ------------------ | ------------ | -------- | ----------------- |
-| `options.bbox` | `BBox` | required | Bounding box |
+| Param | Type | Default | Description |
+| ------------------- | ------------ | -------- | ----------------- |
+| `options.bbox` | `BBox` | required | Bounding box |
| `options.operators` | `Operator[]` | required | Content operators |
**Returns**: `PDFFormXObject`
@@ -837,18 +833,11 @@ Create a reusable Form XObject (content block).
```typescript
const stamp = pdf.createFormXObject({
bbox: { x: 0, y: 0, width: 100, height: 50 },
- operators: [
- ops.setNonStrokingRGB(1, 0, 0),
- ops.rectangle(0, 0, 100, 50),
- ops.fill(),
- ],
+ operators: [ops.setNonStrokingRGB(1, 0, 0), ops.rectangle(0, 0, 100, 50), ops.fill()],
});
const xobjectName = page.registerXObject(stamp);
-page.drawOperators([
- ops.concatMatrix(1, 0, 0, 1, 200, 700),
- ops.paintXObject(xobjectName),
-]);
+page.drawOperators([ops.concatMatrix(1, 0, 0, 1, 200, 700), ops.paintXObject(xobjectName)]);
```
---
diff --git a/content/docs/guides/drawing.mdx b/content/docs/guides/drawing.mdx
index 84df72a..9922944 100644
--- a/content/docs/guides/drawing.mdx
+++ b/content/docs/guides/drawing.mdx
@@ -588,9 +588,7 @@ page.drawRectangle({
});
// Also works with PathBuilder
-page.drawPath()
- .circle(300, 550, 50)
- .fill({ pattern });
+page.drawPath().circle(300, 550, 50).fill({ pattern });
```
See [Low-Level Drawing](/docs/advanced/low-level-drawing) for gradients, tiling patterns, and more.
diff --git a/src/filters/flate-filter.test.ts b/src/filters/flate-filter.test.ts
index 7ff0c07..9d1d42f 100644
--- a/src/filters/flate-filter.test.ts
+++ b/src/filters/flate-filter.test.ts
@@ -113,6 +113,63 @@ describe("FlateFilter", () => {
});
});
+ describe("sync-flush streams", () => {
+ // Some PDF generators (notably PDFium) produce zlib streams terminated
+ // with a sync-flush marker (00 00 FF FF) instead of a proper final
+ // block and Adler-32 checksum. These are the actual byte sequences
+ // from the PDF reported in issue #16.
+
+ it("decodes a small sync-flush stream", () => {
+ // Decompresses to "q\n" — a PDF content stream save-state operator
+ const syncFlush = new Uint8Array([120, 156, 42, 228, 2, 0, 0, 0, 255, 255]);
+
+ const result = filter.decode(syncFlush);
+
+ expect(new TextDecoder().decode(result)).toBe("q\n");
+ });
+
+ it("decodes a larger sync-flush stream with PDF content", () => {
+ // Decompresses to "/ADBE_FillSign BMC \nq \n/Fm0 Do \nQ \nEMC"
+ const syncFlush = new Uint8Array([
+ 120, 156, 210, 119, 116, 113, 114, 141, 119, 203, 204, 201, 9, 206, 76, 207, 83, 112, 242,
+ 117, 86, 224, 42, 84, 224, 210, 119, 203, 53, 80, 112, 201, 87, 224, 10, 84, 224, 114, 245,
+ 117, 6, 0, 0, 0, 255, 255,
+ ]);
+
+ const result = filter.decode(syncFlush);
+
+ expect(new TextDecoder().decode(result)).toBe("/ADBE_FillSign BMC \nq \n/Fm0 Do \nQ \nEMC");
+ });
+
+ it("decodes another small sync-flush stream", () => {
+ // Decompresses to "\nQ\n" — a PDF content stream restore-state operator
+ const syncFlush = new Uint8Array([120, 156, 226, 10, 228, 2, 0, 0, 0, 255, 255]);
+
+ const result = filter.decode(syncFlush);
+
+ expect(new TextDecoder().decode(result)).toBe("\nQ\n");
+ });
+
+ it("still handles well-formed streams normally", () => {
+ const original = new TextEncoder().encode("Hello, World!");
+ const compressed = pako.deflate(original);
+
+ const result = filter.decode(compressed);
+
+ expect(new TextDecoder().decode(result)).toBe("Hello, World!");
+ });
+
+ it("returns empty for truly corrupt data (lenient)", () => {
+ // Random garbage that cannot be decompressed at all.
+ // Returns empty rather than throwing — callers handle empty data gracefully.
+ const garbage = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04]);
+
+ const result = filter.decode(garbage);
+
+ expect(result).toEqual(new Uint8Array(0));
+ });
+ });
+
describe("predictor support", () => {
// Predictor tests would need proper test data
// For now, just verify that params are passed through
diff --git a/src/filters/flate-filter.ts b/src/filters/flate-filter.ts
index 5527dab..973ccbd 100644
--- a/src/filters/flate-filter.ts
+++ b/src/filters/flate-filter.ts
@@ -8,8 +8,13 @@ import { applyPredictor } from "./predictor";
* FlateDecode filter - zlib/deflate compression.
*
* This is the most common filter in modern PDFs. Uses pako for
- * decompression as it handles malformed/truncated data gracefully,
- * unlike native DecompressionStream which can hang indefinitely.
+ * decompression and includes fallback handling for malformed streams.
+ *
+ * Some PDF generators (notably PDFium) produce zlib streams terminated
+ * with a sync-flush marker (00 00 FF FF) instead of a proper final
+ * block and Adler-32 checksum. Standard `pako.inflate()` returns
+ * `undefined` for these streams. We detect this case and recover
+ * the decompressed data from pako's internal state.
*
* Supports Predictor parameter for PNG/TIFF prediction algorithms.
*/
@@ -17,10 +22,7 @@ export class FlateFilter implements Filter {
readonly name = "FlateDecode";
decode(data: Uint8Array, params?: PdfDict): Uint8Array {
- // pako.inflate handles zlib header automatically and gracefully
- // handles truncated/corrupt data (unlike native DecompressionStream
- // which can hang indefinitely on malformed input)
- const decompressed = pako.inflate(data);
+ const decompressed = this.inflate(data);
// Apply predictor if specified
if (params) {
@@ -39,4 +41,63 @@ export class FlateFilter implements Filter {
// Returns zlib format with header
return pako.deflate(data);
}
+
+ /**
+ * Decompress zlib data with fallback for sync-flush terminated streams.
+ *
+ * pako.inflate() returns undefined (instead of throwing) when the
+ * zlib stream ends with a sync-flush marker (00 00 FF FF) and lacks
+ * a proper final deflate block. This is technically invalid per the
+ * zlib spec but common in practice — PDFium and other generators
+ * produce these streams.
+ *
+ * When this happens, we use pako's Inflate class directly and
+ * extract whatever data was successfully decompressed from its
+ * internal output buffer.
+ */
+ private inflate(data: Uint8Array): Uint8Array {
+ // Fast path: standard inflate handles well-formed streams
+ try {
+ const result = pako.inflate(data);
+
+ if (result !== undefined) {
+ return result;
+ }
+ } catch {
+ // pako throws on invalid headers, corrupt data, etc.
+ // Fall through to the recovery path below.
+ }
+
+ // Slow path: recover partial output from malformed streams.
+ //
+ // This handles two cases:
+ // 1. Sync-flush terminated streams (pako.inflate returns undefined):
+ // Some PDF generators (e.g. PDFium) produce zlib streams ending
+ // with a sync-flush marker (00 00 FF FF) instead of a proper
+ // final block. Push without finalization to extract output.
+ // 2. Streams with corrupt headers or checksums (pako.inflate throws):
+ // Try to recover whatever was decompressed before the error.
+ try {
+ const inf = new pako.Inflate();
+ inf.push(data, false);
+
+ // Access pako's internal zlib stream state. The `strm` property
+ // exists at runtime but is not exposed in pako's type definitions.
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
+ const strm = (inf as any).strm;
+ const totalOut = strm.total_out;
+
+ if (totalOut > 0 && strm.output) {
+ // Copy the decompressed bytes out of pako's internal buffer
+ return new Uint8Array(strm.output.slice(0, totalOut));
+ }
+ } catch {
+ // Recovery also failed — truly unrecoverable
+ }
+
+ // No data could be recovered. Return empty rather than throwing to
+ // stay lenient with malformed PDFs — callers (font parsers, content
+ // stream extractors) already handle empty data gracefully.
+ return new Uint8Array(0);
+ }
}
diff --git a/src/integration/signatures/signing.test.ts b/src/integration/signatures/signing.test.ts
index cccd73c..9277b76 100644
--- a/src/integration/signatures/signing.test.ts
+++ b/src/integration/signatures/signing.test.ts
@@ -124,7 +124,7 @@ describe("signing integration", () => {
describe("B-T signing (with timestamp)", () => {
// FreeTSA is a free public timestamp authority
- const tsa = new HttpTimestampAuthority("http://timestamp.sectigo.com");
+ const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr");
it("signs with timestamp (B-T level)", async () => {
const pdfBytes = await loadFixture("basic", "rot0.pdf");
@@ -168,7 +168,7 @@ describe("signing integration", () => {
describe("B-LT signing (long-term validation)", () => {
// FreeTSA is a free public timestamp authority
- const tsa = new HttpTimestampAuthority("http://timestamp.sectigo.com");
+ const tsa = new HttpTimestampAuthority("https://freetsa.org/tsr");
it("signs with timestamp and LTV data (B-LT level)", async () => {
const pdfBytes = await loadFixture("basic", "rot0.pdf");