Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,64 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht

### Added

- Added interactive AcroForm generation (`config.WithAcroForm`, `Paper.SetAcroForm`,
`pkg/forms` field builders) with fill, flatten, and read-back via `pkg/forms.FormFiller`.
- Added PDF/A conformance output (`config.WithPdfA`, levels 1B-4E) with XMP
metadata, extension schemas, and sRGB or custom output intents; Level-A
profiles enable tagged output automatically.
- Added tagged (accessible) PDF output (`config.WithTaggedPDF`, `Paper.SetTagged`)
with structure tree, marked content, and `/Lang` (`config.WithLanguage`).
- Added document-catalog features: viewer preferences (`config.WithViewerPreferences`),
page labels, file attachments, named destinations, page annotations
(`Paper.AddAnnotation` and friends), per-page geometry boxes/rotation,
explicit file IDs (`config.WithFileID`), and deterministic byte-stable builds
(`config.WithDeterministic`).
- Added `pkg/reader` (parse, page remove/reorder/extract, rotate, crop, redact,
`reader.MergeFiles`) backed by new `merge.BytesSelected`/`merge.PageSelection`.
- Added `pkg/sign` (CMS/PAdES digital signatures, timestamps, DSS/LTV), `pkg/svg`,
`pkg/barcode`, `pkg/image` (GIF/WebP/TIFF via codec normalization), `pkg/tmpl`,
and CMYK color support (`props.Color.CMYK`).
- Added HTML pipeline features: CSS multi-column, grid, and position/transform
support, form controls, automatic fallback fonts for non-WinAnsi text
(`html.WithFallbackFontPath`), opt-in remote assets (`html.WithRemoteAssets`,
`html.WithURLPolicy`, `html.WithHTTPClient`), strict asset errors
(`html.WithStrictAssets`), `!important` cascade handling, and typed
`html.ParseError`/`html.AssetError`/`html.LimitError` surfaces.

### Fixed

- Fixed merged PDFs rendering blank: `merge.Bytes` now materializes inherited
page-tree attributes (`/MediaBox`, `/Resources`, `/Rotate`, crop boxes) onto
each copied page.
- Fixed RC4-protected documents corrupting every Info string after the first
(per-string keystream), non-ASCII outline titles showing mojibake (UTF-16
encoding), page-number aliases rendering missing glyphs with UTF-8 fonts,
`SetAlpha` emitting an invalid blend mode for the empty string, XMP metadata
never being referenced from the catalog, bookmark destinations on mixed page
sizes, PDF dates missing timezone offsets, and a potential panic embedding
uncompressed Type1 fonts.
- Fixed silent text corruption: core-font Unicode translation is now
case-insensitive on the family name, and characters outside cp1252 are
reported through `GetReport()` instead of being replaced silently.
- Fixed text layout issues: dash-break line caching, trailing-space alignment
at wrap points, dotted cell borders rendering dashed, checkbox label
encoding and centering, CSS-correct justify (last line not stretched), and
`props.Text` mutation during rendering.
- Fixed HTML/CSS fidelity: `rem`/`in` units, ` ` preservation,
deterministic inline shorthand expansion, `line-height` with units or
percentages, `border` shorthand color functions and width keywords,
`font-size` percentages/keywords, modern color syntax (`rgb(255 0 0 / .5)`,
hue units), `<tfoot>` ordering, body-level inline grouping,
`<ol start="0">`, CSS hex escapes in `content` strings, em-valued heights
and border widths, and malformed box shorthands being dropped instead of
zeroed.

### Security

- Upgraded `golang.org/x/image` to v0.43.0 (fixes GO-2026-5032, GO-2026-4961,
GO-2026-5066, GO-2026-5062 and related decode panics) and pinned the Go
toolchain to 1.26.5.

- Added richer HTML/CSS rendering support for inline boxes, shadows, pseudo-element
image content, flex/table layout, page-break controls, and typography details
including semibold, line-height, letter-spacing, vertical-align, and `nowrap`.
Expand Down
108 changes: 108 additions & 0 deletions annotations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package paper

import "github.com/avdoseferovic/paper/pkg/core/entity"

// AddAnnotation adds a page annotation to the generated PDF.
func (m *Paper) AddAnnotation(annotation entity.PageAnnotation) {
m.config.Annotations = append(m.config.Annotations, entity.CloneAnnotations([]entity.PageAnnotation{annotation})...)
}

// AddLinkAnnotation adds an external URI link annotation to a generated page.
func (m *Paper) AddLinkAnnotation(pageIndex int, rect [4]float64, uri string) {
m.AddAnnotation(entity.PageAnnotation{
PageIndex: pageIndex,
Type: entity.AnnotationLink,
Rect: rect,
URI: uri,
})
}

// AddInternalLinkAnnotation adds an internal named-destination link annotation.
func (m *Paper) AddInternalLinkAnnotation(pageIndex int, rect [4]float64, destName string) {
m.AddAnnotation(entity.PageAnnotation{
PageIndex: pageIndex,
Type: entity.AnnotationLink,
Rect: rect,
DestName: destName,
})
}

// AddPageLinkAnnotation adds an internal link annotation to a zero-based page index.
func (m *Paper) AddPageLinkAnnotation(pageIndex int, rect [4]float64, destPage int) {
m.AddAnnotation(entity.PageAnnotation{
PageIndex: pageIndex,
Type: entity.AnnotationLink,
Rect: rect,
DestPage: &destPage,
})
}

// AddTextAnnotation adds a sticky-note annotation to a generated page.
func (m *Paper) AddTextAnnotation(pageIndex int, rect [4]float64, text, icon string) {
m.AddAnnotation(entity.PageAnnotation{
PageIndex: pageIndex,
Type: entity.AnnotationText,
Rect: rect,
Contents: text,
Icon: icon,
})
}

// AddTextAnnotationOpen adds an initially open sticky-note annotation.
func (m *Paper) AddTextAnnotationOpen(pageIndex int, rect [4]float64, text, icon string) {
m.AddAnnotation(entity.PageAnnotation{
PageIndex: pageIndex,
Type: entity.AnnotationText,
Rect: rect,
Contents: text,
Icon: icon,
Open: true,
})
}

// AddHighlight adds a highlight text-markup annotation.
func (m *Paper) AddHighlight(pageIndex int, rect [4]float64, color [3]float64, quadPoints [][8]float64) {
m.AddTextMarkup(pageIndex, entity.MarkupHighlight, rect, color, quadPoints)
}

// AddUnderline adds an underline text-markup annotation.
func (m *Paper) AddUnderline(pageIndex int, rect [4]float64, color [3]float64, quadPoints [][8]float64) {
m.AddTextMarkup(pageIndex, entity.MarkupUnderline, rect, color, quadPoints)
}

// AddSquiggly adds a squiggly text-markup annotation.
func (m *Paper) AddSquiggly(pageIndex int, rect [4]float64, color [3]float64, quadPoints [][8]float64) {
m.AddTextMarkup(pageIndex, entity.MarkupSquiggly, rect, color, quadPoints)
}

// AddStrikeOut adds a strikeout text-markup annotation.
func (m *Paper) AddStrikeOut(pageIndex int, rect [4]float64, color [3]float64, quadPoints [][8]float64) {
m.AddTextMarkup(pageIndex, entity.MarkupStrikeOut, rect, color, quadPoints)
}

// AddTextMarkup adds a text-markup annotation to a generated page.
func (m *Paper) AddTextMarkup(
pageIndex int,
markupType entity.MarkupType,
rect [4]float64,
color [3]float64,
quadPoints [][8]float64,
) {
types := [...]entity.AnnotationType{
entity.AnnotationHighlight,
entity.AnnotationUnderline,
entity.AnnotationSquiggly,
entity.AnnotationStrikeOut,
}
annotationType := entity.AnnotationHighlight
if markupType >= 0 && int(markupType) < len(types) {
annotationType = types[markupType]
}
m.AddAnnotation(entity.PageAnnotation{
PageIndex: pageIndex,
Type: annotationType,
Rect: rect,
Color: &color,
QuadPoints: append([][8]float64(nil), quadPoints...),
})
}
11 changes: 11 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@ coverage:
precision: 2
round: up
range: "70...100"
status:
project:
default:
target: auto
threshold: 1%
# Patch status is informational: it still reports diff coverage on the PR
# but does not block merges; the project status above keeps overall
# coverage enforced.
patch:
default:
informational: true

ignore:
- ".idea"
Expand Down
56 changes: 56 additions & 0 deletions docs/features/annotations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Annotations

Paper can emit PDF annotations on generated pages by page index. This covers external links, internal named-destination links, sticky notes, and text markup annotations.

## Links

```go
m := paper.New(config.NewBuilder().Build())
m.AddLinkAnnotation(0, [4]float64{40, 50, 120, 70}, "https://example.com")
m.AddInternalLinkAnnotation(0, [4]float64{40, 80, 120, 100}, "section")
m.AddPageLinkAnnotation(0, [4]float64{40, 110, 120, 130}, 1)
```

`AddInternalLinkAnnotation` targets a named destination created with `AddNamedDest` or `WithNamedDestinations`.
`AddPageLinkAnnotation` targets a zero-based page index directly.

## Sticky Notes

```go
m.AddTextAnnotation(0, [4]float64{10, 20, 30, 40}, "Review this section", "Comment")
m.AddTextAnnotationOpen(0, [4]float64{35, 20, 55, 40}, "Open note", "Note")
```

An empty icon defaults to `Note`.

## Text Markup

```go
m.AddHighlight(0, [4]float64{90, 100, 140, 120}, [3]float64{1, 1, 0}, nil)
m.AddUnderline(0, [4]float64{90, 130, 140, 150}, [3]float64{1, 0, 0}, nil)
m.AddSquiggly(0, [4]float64{90, 160, 140, 180}, [3]float64{0, 0, 1}, nil)
m.AddStrikeOut(0, [4]float64{90, 190, 140, 210}, [3]float64{1, 0, 0}, nil)
```

Pass custom quad points when the markup spans multiple text regions.

## Builder API

```go
cfg := config.NewBuilder().
WithAnnotations(entity.PageAnnotation{
PageIndex: 0,
Type: entity.AnnotationText,
Rect: [4]float64{10, 20, 30, 40},
Contents: "Generated by Paper",
Icon: "Comment",
}).
Build()
```

## Notes

- `PageIndex` is zero-based.
- Annotation rectangles and quad points use the generated PDF backend coordinate space.
- Annotations are document-level page entries, so Paper keeps generation on the whole-document path even if concurrent generation was requested.
- This feature emits annotations in newly generated PDFs. It does not yet read, import, flatten, or redact annotations from existing PDFs.
42 changes: 42 additions & 0 deletions docs/features/attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Attachments

Paper can embed files in generated PDFs through the document catalog. Attachments are stored as embedded file streams, referenced from `/Names /EmbeddedFiles`, and listed in the catalog `/AF` associated-files array.

## Builder API

```go
cfg := config.NewBuilder().
WithFileAttachments(entity.FileAttachment{
FileName: "invoice.xml",
MIMEType: "application/xml",
Description: "Invoice XML",
AFRelationship: "Data",
Data: []byte("<invoice/>"),
}).
Build()
```

## Runtime API

```go
m := paper.New(config.NewBuilder().Build())
m.AttachFile(entity.FileAttachment{
FileName: "report.json",
MIMEType: "application/json",
AFRelationship: "Supplement",
Data: payload,
})
```

`AFRelationship` defaults to `Unspecified`, and `MIMEType` defaults to `application/octet-stream`.

For Factur-X/ZUGFeRD-style hybrid invoices, use PDF/A-3B or another
attachment-capable PDF/A profile and set `AFRelationship: "Alternative"` on
the XML attachment. Pair it with `entity.PdfAConfig.XMPSchemas` and
`XMPProperties` to declare the invoice metadata namespace in the PDF/A XMP
packet.

## Notes

- Attachments are document-level catalog entries, so Paper keeps generation on the whole-document path even if concurrent generation was requested.
- This feature embeds files in newly generated PDFs. It does not yet add PDF/A validation, attachment extraction, or reader/import support for existing PDFs.
40 changes: 40 additions & 0 deletions docs/features/destinations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Named Destinations

Named destinations add stable destination names to the generated PDF catalog. Other PDF content or external tools can target these names without relying on physical page labels.

## Builder API

```go
cfg := config.NewBuilder().
WithNamedDestinations(
entity.NamedDestination{Name: "top", PageIndex: 0},
entity.NamedDestination{
Name: "section",
PageIndex: 2,
FitType: entity.DestinationFitH,
Top: 700,
},
).
Build()
```

## Runtime API

```go
m := paper.New(config.NewBuilder().Build())
m.AddNamedDest(entity.NamedDestination{
Name: "detail",
PageIndex: 0,
FitType: entity.DestinationXYZ,
Left: 10,
Top: 20,
Zoom: 1.5,
})
```

`PageIndex` is zero-based. `FitType` defaults to `entity.DestinationFit`; supported values are `DestinationFit`, `DestinationFitH`, and `DestinationXYZ`.

## Notes

- Named destinations are document-level catalog entries, so Paper keeps generation on the whole-document path even if concurrent generation was requested.
- Invalid page indexes and empty destination names are skipped.
39 changes: 39 additions & 0 deletions docs/features/fileid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# File ID

Paper can write an explicit PDF trailer file identifier through `/ID`. This is useful when generated output needs a stable document identifier for downstream workflows.

## Builder API

```go
cfg := config.NewBuilder().
WithFileID([]byte{0xab, 0xcd, 0xef}).
Build()
```

## Runtime API

```go
m := paper.New(config.NewBuilder().Build())
m.SetFileID([]byte{0xab, 0xcd, 0xef})
```

The identifier bytes are written as the first and second entries of the trailer `/ID` array.

## Deterministic Output

```go
cfg := config.NewBuilder().
WithDeterministic(true).
Build()

m := paper.New(cfg)
m.SetDeterministic(true)
```

Deterministic mode derives a stable trailer `/ID` for generated PDFs that do not already have an explicit file ID. It also replaces implicit wall-clock creation, modification, and attachment dates with the zero time so identical inputs produce identical output.

## Notes

- Paper clones the identifier bytes at builder and runtime boundaries.
- When AES-128 protection is enabled after setting a file ID, the identifier is also used by the protection key setup.
- Protected PDFs keep the security handler's encryption-specific identifier behavior.
Loading
Loading