Skip to content

feat(drawing): add transparency groups, soft masks, and gradient stop opacity#39

Open
Mythie wants to merge 1 commit intomainfrom
feat/soft-mask-gradients
Open

feat(drawing): add transparency groups, soft masks, and gradient stop opacity#39
Mythie wants to merge 1 commit intomainfrom
feat/soft-mask-gradients

Conversation

@Mythie
Copy link
Contributor

@Mythie Mythie commented Feb 28, 2026

Implement PDF 1.4+ transparency primitives:

  • Form XObject transparency group support (isolated/knockout/colorSpace)
  • ExtGState soft mask authoring (Luminosity, Alpha, None reset)
  • ColorStop.opacity on gradient APIs with automatic soft-mask composition
  • Draw-time opacity routing: opaque/uniform/varying fast paths
  • Composable graphics state: soft mask + blend mode + constant opacity

Includes comprehensive visual integration tests and structural validation.

image

… opacity

Implement PDF 1.4+ transparency primitives:
- Form XObject transparency group support (isolated/knockout/colorSpace)
- ExtGState soft mask authoring (Luminosity, Alpha, None reset)
- ColorStop.opacity on gradient APIs with automatic soft-mask composition
- Draw-time opacity routing: opaque/uniform/varying fast paths
- Composable graphics state: soft mask + blend mode + constant opacity

Includes comprehensive visual integration tests and structural validation.
@vercel
Copy link
Contributor

vercel bot commented Feb 28, 2026

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

Project Deployment Actions Updated (UTC)
core Ready Ready Preview, Comment Feb 28, 2026 9:54am

@github-actions
Copy link
Contributor

Benchmark Results

Comparison

Load PDF

Benchmark Mean p99 RME Samples
libpdf 2.49ms 4.17ms ±1.7% 201
pdf-lib 38.41ms 42.51ms ±3.4% 14

Create blank PDF

Benchmark Mean p99 RME Samples
libpdf 61μs 132μs ±1.6% 8228
pdf-lib 407μs 1.53ms ±2.4% 1228

Add 10 pages

Benchmark Mean p99 RME Samples
libpdf 102μs 161μs ±1.2% 4896
pdf-lib 514μs 1.91ms ±2.8% 974

Draw 50 rectangles

Benchmark Mean p99 RME Samples
libpdf 185μs 449μs ±0.9% 2698
pdf-lib 1.66ms 5.84ms ±6.2% 302

Load and save PDF

Benchmark Mean p99 RME Samples
libpdf 2.51ms 3.26ms ±1.4% 200
pdf-lib 87.48ms 94.26ms ±3.5% 10

Load, modify, and save PDF

Benchmark Mean p99 RME Samples
libpdf 45.50ms 59.14ms ±10.5% 11
pdf-lib 89.15ms 101.24ms ±5.8% 10

Extract single page from 100-page PDF

Benchmark Mean p99 RME Samples
libpdf 3.72ms 6.48ms ±2.1% 135
pdf-lib 9.03ms 11.51ms ±1.7% 56

Split 100-page PDF into single-page PDFs

Benchmark Mean p99 RME Samples
libpdf 32.86ms 34.56ms ±1.2% 16
pdf-lib 86.85ms 92.29ms ±3.8% 6

Split 2000-page PDF into single-page PDFs (0.9MB)

Benchmark Mean p99 RME Samples
libpdf 623.64ms 623.64ms ±0.0% 1
pdf-lib 1.62s 1.62s ±0.0% 1

Copy 10 pages between documents

Benchmark Mean p99 RME Samples
libpdf 4.52ms 5.19ms ±1.0% 111
pdf-lib 11.79ms 12.78ms ±1.2% 43

Merge 2 x 100-page PDFs

Benchmark Mean p99 RME Samples
libpdf 14.03ms 16.18ms ±1.4% 36
pdf-lib 55.15ms 61.88ms ±3.4% 10
Copying

Copy pages between documents

Benchmark Mean p99 RME Samples
copy 1 page 1.07ms 2.37ms ±4.8% 469
copy 10 pages from 100-page PDF 4.53ms 6.58ms ±2.0% 111
copy all 100 pages 7.29ms 8.05ms ±1.0% 69

Duplicate pages within same document

Benchmark Mean p99 RME Samples
duplicate page 0 892μs 1.61ms ±1.2% 561
duplicate all pages (double the document) 880μs 1.61ms ±1.1% 569

Merge PDFs

Benchmark Mean p99 RME Samples
merge 2 small PDFs 1.43ms 1.93ms ±0.7% 350
merge 10 small PDFs 7.53ms 8.54ms ±1.0% 67
merge 2 x 100-page PDFs 13.52ms 15.36ms ±1.2% 37
Drawing

benchmarks/drawing.bench.ts

Benchmark Mean p99 RME Samples
draw 100 rectangles 311μs 688μs ±2.2% 1608
draw 100 circles 263μs 538μs ±0.8% 1903
draw 100 lines 487μs 1.08ms ±1.4% 1027
draw 100 text lines (standard font) 1.54ms 2.32ms ±1.2% 325
create 10 pages with mixed content 1.19ms 2.11ms ±1.7% 422
Forms

benchmarks/forms.bench.ts

Benchmark Mean p99 RME Samples
get form fields 3.28ms 5.79ms ±3.2% 153
fill text fields 11.22ms 16.03ms ±4.0% 45
read field values 3.01ms 5.25ms ±2.1% 166
flatten form 8.34ms 11.37ms ±2.6% 60
Loading

benchmarks/loading.bench.ts

Benchmark Mean p99 RME Samples
load small PDF (888B) 61μs 158μs ±0.9% 8227
load medium PDF (19KB) 96μs 200μs ±0.7% 5195
load form PDF (116KB) 1.35ms 2.61ms ±2.2% 370
load heavy PDF (9.9MB) 2.35ms 2.84ms ±0.7% 213
Saving

benchmarks/saving.bench.ts

Benchmark Mean p99 RME Samples
save unmodified (19KB) 109μs 267μs ±1.0% 4585
save with modifications (19KB) 717μs 1.35ms ±1.2% 699
incremental save (19KB) 154μs 327μs ±1.0% 3246
save heavy PDF (9.9MB) 2.38ms 2.74ms ±0.5% 211
incremental save heavy PDF (9.9MB) 8.51ms 9.92ms ±2.8% 59
Splitting

Extract single page

Benchmark Mean p99 RME Samples
extractPages (1 page from small PDF) 973μs 1.93ms ±2.5% 514
extractPages (1 page from 100-page PDF) 3.61ms 6.55ms ±2.3% 139
extractPages (1 page from 2000-page PDF) 57.11ms 58.13ms ±0.6% 10

Split into single-page PDFs

Benchmark Mean p99 RME Samples
split 100-page PDF (0.1MB) 30.84ms 35.74ms ±3.5% 17
split 2000-page PDF (0.9MB) 572.88ms 572.88ms ±0.0% 1

Batch page extraction

Benchmark Mean p99 RME Samples
extract first 10 pages from 2000-page PDF 58.05ms 59.48ms ±1.1% 9
extract first 100 pages from 2000-page PDF 62.20ms 65.72ms ±2.4% 9
extract every 10th page from 2000-page PDF (200 pages) 69.31ms 80.33ms ±5.7% 8
Environment
  • Runner: Linux (X64)
  • Runtime: Bun 1.3.10

Results are machine-dependent.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds PDF 1.4+ transparency primitives to the drawing subsystem, enabling soft masks, transparency groups, and per-gradient-stop opacity with automatic draw-time routing for opaque/uniform/varying opacity cases.

Changes:

  • Add ColorStop.opacity and shading-side opacity classification + grayscale “opacity shading” generation for soft-mask composition.
  • Add low-level authoring for transparency groups on Form XObjects and /SMask on ExtGState (including explicit /SMask /None reset).
  • Update high-level drawing/path emission to automatically route varying-opacity shading patterns through soft masks and ensure PDF version >= 1.4 when needed, with new unit + integration tests.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/integration/drawing/low-level-soft-masks.test.ts Adds visual + structural integration coverage for transparency groups, soft masks, and gradient stop opacity.
src/index.ts Re-exports newly added transparency-related types.
src/helpers/pdf-version.ts Adds shared helpers for parsing/bumping PDF catalog version.
src/helpers/pdf-version.test.ts Unit tests for the PDF version helper behavior.
src/drawing/resources/shading.ts Adds stop opacity support, classification, and grayscale opacity shading generation for masks.
src/drawing/resources/shading.test.ts Unit tests for opacity classification and grayscale mask shading dict creation.
src/drawing/resources/pattern.ts Stores optional shading pattern matrix on the resource instance for reuse in opacity-mask patterns.
src/drawing/resources/index.ts Exposes new ExtGState/Form XObject transparency types from the resources barrel.
src/drawing/resources/form-xobject.ts Adds optional resources and group (transparency group) support to Form XObject authoring.
src/drawing/resources/form-xobject.test.ts Unit tests for /Group and /Resources emission behavior.
src/drawing/resources/extgstate.ts Adds /SMask authoring (Alpha/Luminosity/None) + backdrop validation.
src/drawing/resources/extgstate.test.ts Unit tests for soft mask serialization and validation rules.
src/drawing/path-builder.ts Adds a hook to let the page intercept path painting for pattern-opacity routing.
src/api/pdf.ts Centralizes version upgrades, carries shading “definition” metadata, upgrades version on transparency usage, and passes pattern matrix through.
src/api/pdf-page.ts Implements pattern-opacity routing (opaque/uniform/varying) and soft-mask composition at draw time; ensures min PDF version.
.agents/plans/quiet-indigo-fire-transparency-groups-soft-masks-gradient-alpha.md Design/plan documentation for the transparency feature set.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +130 to +145
if (options.subtype === "Luminosity") {
const groupColorSpace = options.group.group?.colorSpace;

if (!groupColorSpace) {
throw new Error(
"Luminosity soft mask requires a form XObject transparency group with a colorSpace",
);
}
}

if (!options.backdropColor) {
return;
}

const groupColorSpace = options.group.group?.colorSpace;

Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateSoftMaskOptions() only enforces a transparency group (options.group.group?.colorSpace) for subtype: "Luminosity" (or when backdropColor is present). Per the PDF soft-mask model, the /G entry in an /SMask should reference a transparency group Form XObject; allowing subtype: "Alpha" with a non-group Form XObject can produce structurally invalid PDFs in some viewers. Consider requiring options.group.group (and thus emitting a /Group dictionary) for both Alpha and Luminosity soft masks, and providing a clear error when the supplied PDFFormXObject lacks transparency-group metadata.

Suggested change
if (options.subtype === "Luminosity") {
const groupColorSpace = options.group.group?.colorSpace;
if (!groupColorSpace) {
throw new Error(
"Luminosity soft mask requires a form XObject transparency group with a colorSpace",
);
}
}
if (!options.backdropColor) {
return;
}
const groupColorSpace = options.group.group?.colorSpace;
const transparencyGroup = options.group.group;
if (!transparencyGroup) {
throw new Error(
`Soft mask of subtype "${options.subtype}" requires the mask form XObject to define a transparency group (group)`,
);
}
const groupColorSpace = transparencyGroup.colorSpace;
if (options.subtype === "Luminosity" && !groupColorSpace) {
throw new Error(
"Luminosity soft mask requires a form XObject transparency group with a colorSpace",
);
}
if (!options.backdropColor) {
return;
}

Copilot uses AI. Check for mistakes.
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.

2 participants