Skip to content

Conversation

@Mythie
Copy link
Contributor

@Mythie Mythie commented Jan 30, 2026

Summary

This PR adds a comprehensive low-level drawing API that exposes PDF content stream operators directly to users, enabling advanced drawing capabilities not possible with the high-level API alone.

Features

  • PDF Operators (ops namespace): ~50 operators for graphics state, path construction, color, text, and XObjects
  • Gradients: Axial (linear) and radial shadings with multi-stop color support
  • Patterns: Tiling patterns and shading patterns for fills and strokes
  • Extended Graphics State: Opacity control and blend modes (Multiply, Screen, Overlay, etc.)
  • Form XObjects: Reusable content stamps
  • Matrix Class: Fluent API for transform composition (translate, rotate, scale)
  • PathBuilder: Chainable path construction with pattern/gradient fills

API Design

Three-layer architecture:

  1. PDF.create*() - Create resources (shadings, patterns, ExtGState, XObjects)
  2. page.register*() - Register resources and get names
  3. page.drawOperators() - Emit operators to content stream
// Example: Rectangle with gradient fill
const gradient = pdf.createAxialShading({
  coords: [0, 0, 100, 0],
  stops: [
    { offset: 0, color: rgb(1, 0, 0) },
    { offset: 1, color: rgb(0, 0, 1) },
  ],
});
const shName = page.registerShading(gradient);

page.drawOperators([
  ops.pushGraphicsState(),
  ops.rectangle(50, 50, 100, 100),
  ops.clip(),
  ops.endPath(),
  ops.paintShading(shName),
  ops.popGraphicsState(),
]);

Changes

  • 38 files changed, ~6500 lines added
  • New files in src/drawing/ for types, operations, factory, path-builder, resources
  • Extended src/api/pdf.ts and src/api/pdf-page.ts with new methods
  • Added src/helpers/operators.ts with comprehensive JSDoc
  • Added src/helpers/matrix.ts for transform composition
  • Split integration tests into 8 focused test files

Testing

  • 2841 tests passing
  • Visual integration tests generate PDFs in test-output/low-level-api/
image

Add comprehensive implementation plan for exposing PDF operators,

Matrix class, gradients, patterns, and form XObjects to enable

advanced drawing capabilities and future Canvas2D package support.
Add primitives for advanced PDF drawing capabilities:
- DrawingFactory for centralized drawing resource management
- Extended graphics state (ExtGState) for opacity and blend modes
- Gradient support (axial/radial shading)
- Resource registration methods on PDFPage
- Colorspace helpers

Reorganize drawing module from src/api/drawing/ to src/drawing/
and move integration tests to src/integration/ directory.

Ref: plan 045-low-level-drawing-api
Session 2 - DX improvements (B+ → A+):
- Fix stale JSDoc examples in pdf-page.ts
- Add labeled tuple types: AxialCoords, RadialCoords, BBox, ColorStop
- Enhance documentation on clip(), pushGraphicsState(), paintXObject()
- Add convenience methods: fillRectWithShading, fillRectWithPattern
- Add PathBuilder integration with fillWithShading/fillWithPattern

Session 3 - Shading patterns (PatternType 2):
- Add PDFShadingPattern type that wraps gradients as usable patterns
- Add createShadingPattern() factory to wrap shadings
- Update PathOptions with pattern/borderPattern properties
- Update wrapPathOps() and PathBuilder.paint() for pattern support
- Deprecate fillWithShading/fillWithPattern in favor of fill({ pattern })

This enables the clean unified API:
  const gradient = pdf.createAxialShading({ ... });
  const pattern = pdf.createShadingPattern({ shading: gradient });
  page.drawPath().circle(x, y, r).fill({ pattern });

All 2843 tests pass.
Remove deprecated PathBuilder methods in favor of the unified pattern API:
- Remove fillWithShading() - use fill({ pattern: pdf.createShadingPattern({ shading }) })
- Remove fillWithPattern() - use fill({ pattern })
- Update JSDoc examples to show the new pattern-based approach
- Remove associated unit tests (4 tests)

The new API is cleaner and more consistent - both tiling patterns and
shading patterns are used the same way via PathOptions.pattern.
Add pattern and borderPattern options to all high-level drawing methods:
- drawRectangle({ pattern, borderPattern, ... })
- drawCircle({ pattern, borderPattern, ... })
- drawEllipse({ pattern, borderPattern, ... })
- drawSvgPath(path, { pattern, borderPattern, ... })

This enables gradient fills on shapes with the simple high-level API:
  const gradient = pdf.createAxialShading({ ... });
  const pattern = pdf.createShadingPattern({ shading: gradient });
  page.drawRectangle({ x, y, width, height, pattern });

Both tiling patterns and shading patterns work with all shape methods.

Includes:
- Updated DrawRectangleOptions, DrawCircleOptions, DrawEllipseOptions, DrawSvgPathOptions types
- Updated RectangleOpsOptions, EllipseOpsOptions in operations.ts
- Updated drawRectangleOps and drawEllipseOps to handle patterns
- Integration tests demonstrating gradient and tiling patterns on all shapes
- Make rotation pivot dots larger and red for visibility
- Add degree symbols to angle labels (0°, 30°, 60°, 90°)
- Add missing 'Rotate 12°' example in Combined Transforms section
- Fix 'Scale+Rot' positioning and label
- Raise angle labels closer to the rectangles
Major layout improvements:
- Rotation section: larger red pivot dots, degree symbols on labels
- Scale section: repositioned with cleaner spacing
- Combined Transforms: 5 examples in a row showing Identity, Scale,
  Rotate, Scale+Rot, and Stretch Y transforms - fixed matrix
  multiplication order (transform * translate, not translate * transform)
- Watermarks: DRAFT and APPROVED centered in their document boxes with
  proper rotation around center point

The key fix was using transform.multiply(Matrix.translate(x, y)) instead
of Matrix.translate(x, y).multiply(transform) - the former applies the
transform then moves to position, while the latter scales the position.
- Fix gradient color stops to sort by offset before processing
- Fix CMYK-to-RGB conversion to use standard formula (1-C)(1-K)
- Document ExtGState opacity clamping behavior in JSDoc
- Extract KAPPA constant to shared helpers/constants.ts
- Standardize JSDoc examples to use ops. prefix consistently
- Split large low-level.test.ts (2187 lines) into 8 focused files
@vercel
Copy link
Contributor

vercel bot commented Jan 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
core Ready Ready Preview, Comment Jan 31, 2026 0:33am

Adds a convenience method to create tiling patterns from embedded images,
similar to CSS background-image with background-repeat. This allows filling
shapes with repeating textures without manually constructing pattern streams.
Replace string-based PDF operators with typed Operator instances from
helpers/operators for better type safety and consistency with the rest
of the codebase. Also removes the now-unused formatNumber helper.
Replace inline ExtGState dict construction with createExtGStateDict
from drawing/factory.ts. This:
- Uses clearer parameter names (fillOpacity/strokeOpacity vs ca/CA)
- Reuses the factory's built-in opacity clamping
- Maintains consistency with PDF.createExtGState() API
…tterns

- Extract getEffectiveBox() to deduplicate width/height getter logic
- Extract computeImageDimensions() for clearer dimension calculation
- Remove redundant variable assignments after instanceof checks
- Consolidate _contentWrapped flag assignment in prependContent()
- Inline simple option extractions in drawSvgPath()
… helpers

- Import serializeOperators from #src/drawing/factory instead of duplicating
- Remove fillRectWithShading and fillRectWithPattern convenience methods
- Remove corresponding tests for removed methods
Replace addGraphicsState (inline dicts) and registerGraphicsStateForOpacity
with a single registerGraphicsState that creates registered refs, enabling
deduplication via registerExtGState.
- Create src/drawing/resources/ directory with separate files:
  - types.ts: BBox, PatternMatrix, BlendMode types
  - operators.ts: serializeOperators utility
  - shading.ts: PDFShading class with static factory methods
  - pattern.ts: PDFTilingPattern, PDFShadingPattern classes
  - extgstate.ts: PDFExtGState class with static factory methods
  - form-xobject.ts: PDFFormXObject class with static factory methods
  - index.ts: re-exports everything

- Simplify class pattern: replace interface + class + factory function
  with single class with static methods (e.g., PDFShading.createAxialDict)

- Delete old monolithic files: factory.ts and resources.ts

- Update imports in pdf.ts and pdf-page.ts to use new module structure
…istency

Low-level ops interfaces now consistently use strokeWidth across all
shape types (Rectangle, Ellipse, Path, Line). The high-level DrawLineOptions
still uses thickness since it's semantically appropriate for lines.
- Change BBox from tuple [x, y, width, height] to object interface
- Make BoundingBox a type alias for BBox (same shape, both exported)
- Fix PDF serialization to correctly convert to corners [llx, lly, urx, ury]
- Update all usages in patterns, form xobjects, and tests

This addresses H2 and H3 from the code review: the object form makes it
clear you're dealing with width/height (not x1,y1,x2,y2), and the
serialization now correctly converts to PDF spec corner format.
Six register methods (Font, Image, Shading, Pattern, ExtGState, XObject)
shared ~30 lines of identical logic. Consolidating into a single helper
reduces duplication and ensures consistent behavior across resource types.
…ilder

- Move serializeOperators from resources/operators.ts to drawing/serialize.ts
  for better discoverability (M3)
- Use serializeOperators directly in PathBuilder.emitOps instead of
  string round-trip via toString() for better performance (M6)
@Mythie Mythie merged commit 334789d into main Jan 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant