pdf

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package pdf is the GoFastr PDF viewer / editor / redactor plugin. It mounts pdf.js (render) + pdf-lib (write) inside an opaque-origin sandboxed iframe, exactly like the richtext/mermaid/monaco plugins, and adds a fourth document shape: the canonical doc is an annotation OVERLAY (schema pdf-v1), never the file bytes. The PDF itself is an external resource the host resolves via WithSource and pushes over the postMessage bridge — the frame has connect-src 'none' and fetches nothing, which is the structural reason a confidential document opened for redaction cannot be exfiltrated.

Mode (view / annotate / redact) is host-chosen and enforced on BOTH sides: the mode is bridged to the frame in init.config so it can hide its own UI, AND the Go handlers reject any payload the mode does not permit. UI-only gating is explicitly forbidden by the platform rules — a de-opaqued frame or a hand-crafted postMessage must not reach a mode-disallowed action. See docs/pdf.md and docs/plugin-platform.md.

Index

Constants

View Source
const (
	PdfjsVersion  = "6.2.108"
	PdfLibVersion = "1.17.1"
)

PdfjsVersion is the pdf.js release compiled into the frame bundle, and PdfLibVersion the writer used to rebuild a redacted document. Both are stated on the demo page, and TestDemoPageStatesTheBundledLibraryVersions requires them to match js/package.json — mermaid's page shipped a version twelve releases stale because nothing checks prose.

View Source
const (
	ExportKindExport   = "export"   // produce + store the file
	ExportKindDownload = "download" // produce + return a download URL
	ExportKindPrint    = "print"    // produce for the host print path
	ExportKindRedact   = "redact"   // produce a redacted document (ModeRedact only)
)

ExportKind values the mode enforcement branches on. They are the only kinds the frame is permitted to request; any other value is rejected as a bad request.

View Source
const (
	Name             = "pdf"
	Version          = "0.1.0"
	RoutePrefix      = "/__gofastr/plugin/pdf"
	ViewerHTMLURL    = RoutePrefix + "/viewer.html"
	ViewerJSURL      = RoutePrefix + "/viewer.js"
	ViewerCSSURL     = RoutePrefix + "/viewer.css"
	AdapterScriptURL = RoutePrefix + "/adapter.js"
	ConfigScriptURL  = RoutePrefix + "/config.js"
	SamplePDFURL     = RoutePrefix + "/sample.pdf"
	SaveURL          = RoutePrefix + "/save"
	ExportURL        = RoutePrefix + "/export"
	DocRoute         = RoutePrefix + "/doc/{id}"
	DemoURL          = "/pdf"
	SchemaVersion    = "pdf-v1"

	// CapPDFExport gates the /export route. It is OPTIONAL — it is NOT in
	// [DefaultCapabilities]. [WithExportHandler] appends it (mirroring geomap's
	// WithSearch → geocode:search), because producing a PDF is egress the host
	// explicitly turned on. ModeRedact requires it (redaction produces a new
	// file), so constructing ModeRedact without it panics at [New].
	CapPDFExport = "pdf:export"
)

Identity and route constants. Both this plugin and host/adapter.js hard-code these exactly (protocol-v1.md §2/§10). The demo lives at /pdf.

These mirror the pdf row in plugins.json; internal/registry tests pin Name + RoutePrefix against that row, so they MUST NOT drift.

Variables

View Source
var ErrConflict = errors.New("pdf: save conflict")

ErrConflict is the sentinel a WithSaveHandler hook returns to signal that the save lost an optimistic-concurrency check — the stored document changed under the editor since it loaded (the overlay's rev is stale). handleSave maps it to HTTP 409 (E_CONFLICT) rather than the generic 500 (E_SAVE), which is the one status the host adapter relays back to the frame as a distinct saveResult so the editor can keep the doc dirty and warn the user instead of silently dropping their edits — the exact contract richtext and monaco ship. Wrap it (fmt.Errorf("...: %w", pdf.ErrConflict)) to add context; handleSave uses errors.Is.

Functions

func DefaultCapabilities

func DefaultCapabilities() []string

DefaultCapabilities is the always-on grant set advertised to the viewer. These mirror the pdf row's "capabilities" in plugins.json. pdf:export is deliberately absent here — it is an optionalCapability, appended by WithExportHandler.

func Mount

func Mount(cfg MountConfig) render.HTML

Mount renders the generic mount marker. The host adapter reads the doc id off the marker to fetch /doc/{id}; the overlay round-trips through the hidden field on a normal form POST. Drop it into a form. All interpolated values are HTML-escaped via render.Escape inside pluginhost.MountMarker.

func SampleDocument

func SampleDocument() []byte

SampleDocument returns the embedded two-page sample PDF the demo renders — real selectable text (including the SPIKE_SECRET_ALPHA marker the tests look for) plus an embedded raster image.

Exported for demos, probes and tests that wire their own WithSource and still want the stock document for every id they do not special-case. A WithSource hook fully REPLACES the default, and returning (nil, nil) means "no such document" (404) rather than "fall back to the sample" — a production host must never silently serve a demo file in place of a document it failed to find, so the fallback is opt-in and explicit.

The returned slice is a copy; the embedded bytes are not mutable by callers.

func UIHostOption

func UIHostOption() uihost.Option

UIHostOption injects the platform broker, this plugin's config script, and this plugin's adapter (in that order — the adapter reads the config global the config script publishes, and registers with the broker the former defines).

Types

type Annotation

type Annotation struct {
	// ID is the stable client-assigned identity (so a re-save updates an
	// existing annotation rather than duplicating it).
	ID string `json:"id,omitempty"`
	// Page is the 1-indexed page number the annotation lives on.
	Page int `json:"page,omitempty"`
	// Type is the annotation kind ("highlight", "stamp", "drawing", …). The
	// set is open: an unknown type round-trips without rejection.
	Type string `json:"type,omitempty"`
	// Rect is the hit box in PDF user space as [x, y, w, h] (origin
	// bottom-left). A slice rather than a fixed [4]float64 so a malformed
	// entry cannot fail the whole overlay parse — the raw DocJSON is the
	// authoritative record either way.
	Rect []float64 `json:"rect,omitempty"`
}

Annotation is one markup item on a page. The Rect is the common hit box; type-specific extras (color, stroke width, label text, …) survive verbatim in Raw for round-trip fidelity.

type ExportRequest

type ExportRequest struct {
	DocID string // persistence key
	Kind  string // "export" | "download" | "print" | "redact"
	Bytes []byte // the produced PDF bytes
	// Filename is the frame's suggested name, already sanitised of path
	// separators and Content-Disposition metacharacters. It is a HINT: the
	// handler decides where bytes actually go, and may ignore it. Empty when
	// the frame offered none.
	Filename string
	// Report is the in-frame verification report — for a redact export, the
	// evidence that the content under every rect is gone. Opaque JSON: the
	// frame owns its shape.
	//
	// It rides an HTTP header, so it is bounded. When a report would exceed
	// that bound the adapter substitutes a compact record carrying the same
	// verdicts plus "truncated":true — the detail can be lost, the VERDICT
	// never is, and a host must not read a missing report as a pass. Nil means
	// the frame sent none at all (a plain export, not a redaction).
	Report json.RawMessage
}

ExportRequest is the payload handed to WithExportHandler. Bytes is the produced PDF (already rasterized for redacted pages); Report is the in-frame verification report (opaque JSON — the frame owns its shape); Kind is the export intent, which the route has already mode-checked.

type Mode

type Mode uint8

Mode is a bitmask of the host-selected capabilities the frame may exercise. It is host-chosen only — never plugin- or user-selectable — and normalises to a lattice: ModeRedact ⊇ ModeAnnotate ⊇ ModeView. A host composes the surface it wants with WithMode; the handlers enforce each route against the bits actually set.

const (
	// ModeView permits rendering only. /save and /export are both rejected.
	ModeView Mode = 1 << iota
	// ModeAnnotate permits overlay edits (annotations, form fills, page ops)
	// and non-redacting export. Implies ModeView.
	ModeAnnotate
	// ModeRedact permits destructive redaction export on top of everything
	// annotate allows. Implies ModeAnnotate (and ModeView). Requires the
	// pdf:export capability — [New] panics if it is absent.
	ModeRedact
)

The three mode bits. They are OR-composable (WithMode accepts ModeView|ModeAnnotate|ModeRedact), but [normalizeMode] folds any combination into the lattice so the enforcement helpers can test a single bit per route.

func (Mode) String

func (m Mode) String() string

String renders the mode as the highest tier its bits grant, which is what the frame advertises in init.config.mode ("view" | "annotate" | "redact"). The frame shows exactly the UI that tier unlocks; the Go side still enforces the individual bits.

type MountConfig

type MountConfig struct {
	DocID     string // persistence key (default "demo")
	MinHeight string // initial iframe height before first resize (default "480px")
	Doc       string // optional initial overlay JSON, server-rendered for reload round-trip
}

MountConfig configures Mount.

type Option

type Option func(*Plugin)

Option configures a Plugin.

func WithCapabilities

func WithCapabilities(caps ...string) Option

WithCapabilities overrides the grant set advertised to the viewer. Default: DefaultCapabilities. Note pdf:export is appended separately by WithExportHandler even when this fully replaces the set — the gate is on egress the host explicitly enabled, so silently dropping it would just break export with a 412.

func WithDemoPage

func WithDemoPage() Option

WithDemoPage registers the self-contained themed demo page at DemoURL.

func WithDemoRoute

func WithDemoRoute(path string) Option

WithDemoRoute overrides where WithDemoPage mounts the demo (default DemoURL, "/pdf"). A host that wants "/pdf" for its own page moves the demo aside with this.

func WithDevGrantAll

func WithDevGrantAll() Option

WithDevGrantAll short-circuits the capability gate (Phase-0 demo / tests).

func WithExportHandler

func WithExportHandler(fn func(ctx context.Context, req ExportRequest) (string, error)) Option

WithExportHandler overrides the export hook AND opts the plugin into the optional pdf:export capability (appended to the grant set if not already present — mirroring geomap's WithSearch → geocode:search). The default handler returns a data: URL echoing the bytes, enough to prove the round trip; a production host points this at its file store and returns a real URL. ModeRedact requires pdf:export, so a host selecting ModeRedact MUST also supply an export handler (or explicitly grant the capability).

func WithMaxBytes

func WithMaxBytes(n int64) Option

WithMaxBytes is the host-enforced ceiling on the size of a PDF the plugin will move through /doc/{id} and /export. It is checked BEFORE bytes are relayed into the frame (413 rather than streaming a huge file into a postMessage) so an oversized document is rejected at the boundary. Default 32 MiB.

func WithMode

func WithMode(m Mode) Option

WithMode sets the host-selected capability surface. It is the ONLY way to choose a mode — modes are never plugin- or user-selectable. The value is normalised to the view⊆annotate⊆redact lattice. Default ModeView.

func WithRedactDPI

func WithRedactDPI(dpi int) Option

WithRedactDPI sets the rasterization DPI for pages that carry a redaction (pages without one are copied through losslessly). The valid range is 72..600; New panics outside it because a too-low DPI silently blurs redactions (content leaks visually) and a too-high one bloats the file for no gain. Default 200.

func WithSaveHandler

func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option

WithSaveHandler overrides the persistence hook. The default stores the canonical overlay JSON in an in-memory map keyed by DocID.

func WithSource

func WithSource(fn func(ctx context.Context, id string) ([]byte, error)) Option

WithSource installs the document resolver backing GET /doc/{id}. The default serves the embedded sample.pdf, which is enough for the demo and tests; a production host points this at its document store. The function runs in the host page with the session and CSRF token attached — the frame cannot call /doc/{id} (connect-src 'none'), so authorization stays here at the data layer.

type Overlay

type Overlay struct {
	// SchemaVersion is the interchange version ("pdf-v1"). Echoed verbatim.
	SchemaVersion string `json:"schemaVersion,omitempty"`
	// Src binds the overlay to the external PDF it was authored against. The
	// frame recomputes src.sha256 on load and refuses to apply annotations on
	// mismatch, so an overlay never silently paints boxes at stale coordinates.
	Src Source `json:"src,omitempty"`
	// Annotations is the ordered list of markup (highlights, stamps, drawings,
	// …). Only the common fields are typed; see the note on [Annotation] for
	// where the type-specific ones live.
	Annotations []Annotation `json:"annotations,omitempty"`
	// FormFields maps an AcroForm field name to its value. Values are opaque
	// JSON (string / bool / number / …): typed as json.RawMessage rather than
	// any so the struct stays precise without modelling every field shape.
	FormFields map[string]json.RawMessage `json:"formFields,omitempty"`
	// Redactions is the ordered list of regions to remove at export. Redaction
	// is destructive and irreversible; each rect may carry a reason label.
	Redactions []Redaction `json:"redactions,omitempty"`
	// PageOps is the ordered list of page operations (rotate / delete / move /
	// insert / append) applied at export.
	PageOps []PageOp `json:"pageOps,omitempty"`
	// Rev is the optimistic-concurrency revision. A save whose rev does not
	// match the stored revision loses the race and surfaces as [ErrConflict]
	// (HTTP 409), so two editors cannot silently clobber each other.
	Rev int `json:"rev,omitempty"`
}

Overlay is the canonical pdf-v1 document handed to WithSaveHandler and surfaced from Plugin.LoadDoc. It is the annotation layer over an external PDF, never the file bytes.

type PageOp

type PageOp struct {
	// Op is the operation: "rotate", "delete", "move", "insert", or "append".
	Op string `json:"op,omitempty"`
	// Page is the 1-indexed target page (the page being rotated/deleted/moved,
	// or the anchor for insert/append).
	Page int `json:"page,omitempty"`
	// Value is the operation parameter (e.g. rotation degrees for "rotate", or
	// the source page for "move"/"insert").
	Value int `json:"value,omitempty"`
}

PageOp is one ordered page mutation applied at export.

type Plugin

type Plugin struct {
	// contains filtered or unexported fields
}

Plugin is the PDF viewer / editor / redactor plugin. It implements framework.Plugin and mirrors the richtext/monaco shape (opaque-origin sandboxed iframe, protocol v1 over postMessage, go:embed'd frame bundle, capability gate, save/export handlers) with the pdf additions: modes, an optional export capability, and a host-resolved document source.

func New

func New(opts ...Option) *Plugin

New constructs a Plugin. All fail-loud validation runs here so a misconfiguration aborts construction rather than silently de-opaquing the frame, leaking a too-low redaction DPI, or mounting ModeRedact without the export capability it requires.

func (*Plugin) Capabilities

func (p *Plugin) Capabilities() []string

Capabilities returns the grant set this plugin advertises to the viewer.

func (*Plugin) Init

func (p *Plugin) Init(app *framework.App) error

Init registers every asset and RPC route on the app's router.

func (*Plugin) LoadDoc

func (p *Plugin) LoadDoc(ctx context.Context, docID string) (docJSON string, ok bool)

LoadDoc returns the last-saved overlay JSON for docID (demo round-trip). ok is false when the doc has never been saved.

func (*Plugin) Manifest

func (p *Plugin) Manifest() pluginhost.Manifest

func (*Plugin) MaxBytes

func (p *Plugin) MaxBytes() int64

MaxBytes returns the host-enforced byte ceiling.

func (*Plugin) Mode

func (p *Plugin) Mode() Mode

Mode returns the host-selected, lattice-normalised mode. It is bridged to the frame via init.config.mode so the frame can hide UI the mode does not grant.

func (*Plugin) Name

func (p *Plugin) Name() string

type Redaction

type Redaction struct {
	ID     string    `json:"id,omitempty"`
	Page   int       `json:"page,omitempty"`
	Rect   []float64 `json:"rect,omitempty"`
	Reason string    `json:"reason,omitempty"`
}

Redaction is one destructive removal region. At export the content under Rect is excised: pages with any redaction are rasterized at WithRedactDPI, masked, and embedded as images into a freshly built document; pages without one are copied through losslessly.

type SaveRequest

type SaveRequest struct {
	DocID         string  // persistence key (the mount marker's data-fui-plugin-docid)
	Doc           Overlay // parsed overlay (zero-valued if the body failed to parse)
	DocJSON       string  // raw canonical overlay JSON (verbatim, authoritative)
	SchemaVersion string  // interchange version ("pdf-v1")
	Rev           int     // optimistic-concurrency revision carried by the overlay
}

SaveRequest is the persistence payload handed to WithSaveHandler. Doc is the parsed overlay (typed access for inspection / mode checks); DocJSON is the raw canonical JSON for verbatim persistence — the authoritative record that round-trips through the hidden field, so type-specific annotation extras never get dropped by a struct re-marshal.

type Source

type Source struct {
	// Kind is how the host resolves the PDF: "url" (a fetchable location) or
	// "id" (an opaque key the host's [WithSource] understands). The host
	// adapter fetches /doc/{ref}; the frame never does.
	Kind string `json:"kind,omitempty"`
	// Ref is the url or id; it is the {id} path segment of GET /doc/{id}.
	Ref string `json:"ref,omitempty"`
	// SHA256 binds the overlay to exact bytes. On load the frame recomputes it
	// and, on mismatch, refuses to apply annotations (soft warning over plain
	// http:// to a non-localhost host — never a hard fail).
	SHA256 string `json:"sha256,omitempty"`
	// Pages is the page count of the referenced PDF, cached in the overlay so
	// the frame can lay out the page rail before the bytes arrive.
	Pages int `json:"pages,omitempty"`
}

Source identifies the external PDF an overlay belongs to.

Directories

Path Synopsis
cmd
spike command
Command spike serves ONLY the pdf plugin (with its demo page) for the throwaway WebKit probe at pdf/js/spike-webkit.mjs.
Command spike serves ONLY the pdf plugin (with its demo page) for the throwaway WebKit probe at pdf/js/spike-webkit.mjs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL