From 24b732039471844129c3e9455bb262a7f0708fce Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 11:16:08 +1100 Subject: [PATCH 1/4] fix(filters): handle sync-flush terminated zlib streams in FlateDecode pako.inflate() returns undefined (rather than throwing) for zlib streams terminated with a sync-flush marker (00 00 FF FF) instead of a proper final block. Some PDF generators like PDFium produce these streams. The undefined result propagated through FilterPipeline and caused a TypeError in downstream code trying to access .length on undefined chunks. FlateFilter.decode() now detects this case and recovers the decompressed data from pako's internal output buffer. Truly corrupt streams return empty rather than throwing, consistent with the library's lenient approach to malformed PDFs. Closes #16 --- src/filters/flate-filter.test.ts | 57 +++++++++++++++++++++++++ src/filters/flate-filter.ts | 73 +++++++++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 6 deletions(-) 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); + } } From 30cee71123c41dce86c6bd17d877bccc1de5176a Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 11:16:38 +1100 Subject: [PATCH 2/4] chore: gitignore fixtures/private/ --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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/ From 0f1096d95caf6dfc3fb1556f0a4ea80cffd788f5 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 11:20:29 +1100 Subject: [PATCH 3/4] chore: change testing timestamp authority --- src/integration/signatures/signing.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"); From ae3aac4a518599f64d07f43a0d6a9c6028ac203d Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Sun, 8 Feb 2026 11:20:51 +1100 Subject: [PATCH 4/4] chore: formatting --- content/docs/advanced/low-level-drawing.mdx | 144 ++++++++++---------- content/docs/api/pdf-page.mdx | 32 ++--- content/docs/api/pdf.mdx | 75 +++++----- content/docs/guides/drawing.mdx | 4 +- 4 files changed, 119 insertions(+), 136 deletions(-) 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.