pdfdoc

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package pdfdoc is the only package in Byblos that imports pdfcpu.

Everything above it speaks in the types declared here, so replacing the underlying PDF library is a change to this package alone (design spec section 3). arch_test.go in the repository root enforces that.

pdfcpu API notes, verified against v0.13.0:

  • api.ReadAndValidate dereferences conf.Cmd with no nil check and pdfcpu's fault.Catch only recovers its own panic type, so passing a nil *model.Configuration kills the process rather than returning an error. Every call here passes defaultConfig().
  • model.NewDefaultConfiguration builds pdfcpu's package-level default LAZILY, on first use, and writes it with no synchronisation, so two goroutines opening documents at once race inside pdfcpu itself. defaultConfig forces that init exactly once; see its comment (byb-a5z).
  • ctx.PageCount is zero after ReadContext; ctx.EnsurePageCount() populates it.
  • types.StreamDict.Content is empty until Decode() is called.
  • model.Image.Width/Height/Bpc are zero unless ExtractImage is called with stub=true, and a stub carries no pixels. Pixel dimensions therefore come from the image XObject's own stream dictionary.
  • pdfcpu cannot decode JBIG2Decode or JPXDecode and returns the raw opaque bytes with FileType "jbig2"/"jpx" rather than erroring. Callers must check the file type; extract.go does.
  • pdfcpu.RenderImage returns (nil, "", nil) for any other unhandled filter, and ExtractImage passes that straight through as a model.Image with a nil embedded Reader. Reading it panics, so RawImage guards and returns ErrUnsupportedCodec instead.
  • types.Dict's typed accessors (IntEntry, DictEntry, ArrayEntry, ...) do NOT dereference an indirect reference; they return zero. Real documents do use an indirect /Resources on a Form XObject, so every dictionary read that feeds a Byblos type goes through the deref helpers at the bottom of this file.
  • pdfcpu's own page tree walk falls into that same trap: it reads /Kids with ArrayEntry, so a /Pages node holding an indirect /Kids looks childless and PageDict returns that node as the dictionary of every page, with no error. Open repairs the tree before anything walks it; see normalizePageTree.

Index

Constants

This section is empty.

Variables

View Source
var ErrFontCensusUnreadable = fmt.Errorf("pdfdoc: font dicts are not readable")

ErrFontCensusUnreadable reports a file whose font dicts could not be read trustworthily. It is deliberately distinct from "a file with no fonts": counting the first as the second is silent, because the totals still look plausible and only the per-file denominator goes wrong.

View Source
var ErrMalformed = errors.New("byblos/pdfdoc: malformed document")

ErrMalformed reports a document pdfcpu could not parse without panicking.

pdfcpu indexes unchecked in several parsers — skipTJ walks off an empty operand slice on a malformed TJ array. Its own fault.Catch recovers only its own panic type, so nothing below this package stops it.

skipTJ is reached from pdfcpu's parseContent, which runs only when PageDict is asked to consolidate resources. Page and Annots no longer ask (byb-ged), so that crash no longer arrives from what looks like a dictionary read — but Optimize still reaches it, because api.Optimize calls PageDict(i, true) itself.

A library for processing archives cannot die on one damaged file out of five thousand, which is what this cost before byb-avp: a whole run lost, and not even a counter left behind to say a page had been skipped. Recovering at this seam is what makes a malformed page an outcome the caller can count rather than an exit. It is the same boundary ErrUnsupportedCodec sits on — pdfcpu's misbehaviour becomes a Byblos error here or nowhere.

View Source
var ErrUnbalancedContent = errors.New("byblos/pdfdoc: unbalanced q/Q in content stream")

ErrUnbalancedContent reports a content stream whose q/Q nesting does not come back to zero, in either direction. A surplus Q pops a state WrapContent's own "before" wrapper pushed, so everything painted after that point in the stream is silently NOT rotated -- a page that comes out half-corrected, with no error. A surplus q is merely malformed. Both are refused, because a wrapper that only half-applies is worse than a refusal.

View Source
var ErrUnsupportedCodec = errors.New("byblos/pdfdoc: image codec cannot be rendered")

ErrUnsupportedCodec reports an image stream whose compression filter pdfcpu will not render. It exists so that pdfcpu's nil-reader return becomes an error at this seam instead of a nil dereference in the caller.

It is NOT returned for JBIG2 or JPX: those come back as real bytes with a file type naming the codec, and deciding what to do about them is extract.go's job, not this package's.

Functions

func BuildFromPages added in v0.3.0

func BuildFromPages(w io.Writer, pages []PageSource) error

BuildFromPages writes a document whose page i is pages[i].

Deleting a page is omitting it from the sequence, reordering is ordering the sequence, inserting is naming a different Source, and rotating is a field. The output is a new document every time; no Source is modified.

It writes nothing to w unless the whole document was built.

func BuildFromPagesWithProperties added in v0.3.0

func BuildFromPagesWithProperties(w io.Writer, pages []PageSource, properties map[string]string) (err error)

BuildFromPagesWithProperties is BuildFromPages, with properties merged into the output's Info dictionary as part of the SAME build and the SAME write (byb-yul.6, Correction 5).

THIS IS WHY IT EXISTS, RATHER THAN A CALLER STAMPING PROPERTIES ON AFTERWARDS. BuildFromPagesContext (editpages.go) used to build the document here and then pipe the bytes through WriteProperties for its provenance record -- and WriteProperties goes through pdfcpu's own read-validate-optimize-write pass, the SAME catalog-traversal writer this package's own writer exists to replace. Measured: every dangling-reference shape buildpages.go's package comment describes came BACK on that second pass, even when the first pass was this package's self-write. Folding the properties into the one build this package already does removes the second pass -- and the bug it reintroduces -- entirely, rather than swapping its write half for this package's own (which was measured to regress the output size of an unrelated, widely-shared caller: Optimize's delinearize-and-rewrite path pipes an ALREADY re-written document, often already using compressed object streams, through WriteProperties for its own provenance stamp, and this package's writer never emits object streams -- +48% on one pdfcpu test fixture, enough to trip Optimize's never-larger-than-input fallback and silently stop delinearizing it). BuildFromPages' own context has no such document to preserve: it is built fresh, object stream or not, every time.

properties is encoded exactly the way pdfcpu's own PropertiesAdd encodes it (types.EscapedUTF16String), so ReadProperties -- unchanged, and still pdfcpu's own reader -- decodes it back exactly as it always has. A key already claimed by carryInfo's allowlist or by the /Producer stamp is silently overwritten by properties, last-write-wins; nothing today passes one of those keys here.

func IsWrongPassword added in v0.3.0

func IsWrongPassword(err error) bool

IsWrongPassword reports whether err is pdfcpu's own refusal to open a document because neither an empty user nor an empty owner password authenticated -- as opposed to any other reason Open can fail (a malformed file, an unsupported /V, ...), which is not evidence of a password.

func Linearize

func Linearize(rs io.ReadSeeker, w io.Writer) (err error)

Linearize writes a linearized ("fast web view") copy of rs's PDF to w.

It is the last thing that may touch the bytes: anything that re-serializes the document afterwards -- including WriteProperties, which goes through pdfcpu's writer -- silently removes the linearization again.

func Optimize

func Optimize(rs io.ReadSeeker, w io.Writer) (err error)

Optimize runs pdfcpu's structural optimize pass over rs and writes the result to w. It validates rs first (api.Optimize is ReadValidateAndOptimize + WriteContext underneath), so a malformed or non-PDF rs is reported here rather than producing corrupt output.

func ReadProperties

func ReadProperties(rs io.ReadSeeker) (map[string]string, error)

ReadProperties returns rs's Info-dictionary properties. A key WriteProperties never wrote is simply absent from the result, not an error.

func Validate

func Validate(r io.ReadSeeker) (err error)

Validate reports whether r parses as a structurally valid PDF: xref offsets resolve and the page tree's /Count agrees with its /Kids. It checks structure only, not pixel content — a stream whose filter claims a codec its bytes do not actually hold still validates, and so does a stream whose /Length is wrong but still parses (pdfcpu trusts the declared /Length when reading; it does not recompute it from the bytes between `stream` and `endstream`).

This exists for BuildPDF (byb-c3o): a hand-rolled writer has no reader of its own to round-trip through, and pdfcpu's validator is the independent check that the bytes it emits are a PDF at all. The one property this cannot check — /Length matching the stream — is instead guaranteed by construction in pdfbuild's writer: fillStream writes /Length from len(payload) immediately before writing payload itself (internal/pdfbuild/pdfbuild.go), so the two cannot diverge without editing that one function.

func ValidatePages added in v0.4.0

func ValidatePages(pages []PageSource) error

ValidatePages rejects what cannot be resolved without opening a document.

EXPORTED FOR A PRE-BUILD CHECK (gap G2). byblos.ValidatePages is a caller's only way to learn that an edit list is unbuildable without paying for the build; see that wrapper for what this check does NOT cover.

The rotation check is not defensive tidiness. pdfcpu writes /Rotate 45 and /Rotate -90 with a nil error, and the 45 case produces a file pdfcpu itself then refuses on re-read -- so an unchecked rotation is a document that fails at the next reader rather than at this call. 360 is rejected with the rest: it is a legal quarter-turn multiple and it is not one of the four values this takes, and silently folding it to 0 would accept 720 too.

PageInfo.Rotate is NOT a safe way to produce this field. It normalizes into [0,360), so a declared -90 reads back as 270, and it does not guarantee a multiple of 90 -- a declared 45 reads back as 45. A caller round-tripping through it must still be told when the value it got cannot be written.

func WriteProperties

func WriteProperties(rs io.ReadSeeker, w io.Writer, properties map[string]string) error

WriteProperties adds properties to rs's Info dictionary and writes the result to w, replacing any existing entries with the same keys.

Types

type Annot

type Annot struct {
	Subtype string // /Subtype; "" when absent
	Rect    Rect   // /Rect, normalised so LL really is the minimum corner
	HasRect bool   // a well-formed 4-number /Rect was present
	Flags   int    // /F; ISO 32000-1 Table 165. 0 when absent
	HasAP   bool   // /AP present
	HasAPN  bool   // /AP /N resolves to a stream, or /AS selects one that does
	HasOC   bool   // /OC present: visibility depends on optional-content state
}

Annot is one annotation as its own dictionary declares it, in the same default user space as Page.MediaBox: points, origin lower-left.

Like ImageInfo, this records declarations rather than content. Whether the annotation's appearance stream actually deposits ink where Rect says it does needs a renderer, and design spec section 2 puts that out of scope; for deciding whether Byblos is dropping something a viewer would show, "it declares a normal appearance and a box" is the whole answer.

func (Annot) Paints

func (a Annot) Paints() bool

Paints reports whether this annotation puts marks on the page.

It is deliberately conservative in one direction only: every reason to say no is a fact in the dictionary, and anything unrecognised counts as painting. Over-counting shows up as a page needlessly flagged; under-counting is the silent data loss this measurement exists to find.

Reason returns the bucket name, so a caller can report why rather than only how many.

func (Annot) Reason

func (a Annot) Reason() string

Reason names why the annotation deposits nothing, or "" when it paints.

type ColorSpace

type ColorSpace struct {
	Name string

	// Indexed only.
	Base   string // the base space the palette entries are expressed in
	HiVal  int    // the highest palette index; the palette has HiVal+1 entries
	Lookup []byte // (HiVal+1) * components(Base) bytes
}

ColorSpace is the colour space of a substituted image.

Name is a device space ("DeviceGray", "DeviceRGB", "DeviceCMYK") or "Indexed", in which case Base, HiVal and Lookup describe the palette. Indexed is here because a quantized image is the shape B3 produces, and a seam that only spoke DeviceGray would have to be reopened for it (byb-0he).

type DanglingRef added in v0.3.0

type DanglingRef struct {
	Object int // the object holding the reference
	Target int // the object number it names
}

DanglingRef is one reference naming an object the document does not define.

func DanglingRefs added in v0.3.0

func DanglingRefs(out []byte) ([]DanglingRef, error)

DanglingRefs re-reads out and reports every DanglingRef in it, sorted for a deterministic diff. ISO 32000-1 7.3.10 makes a reference to an undefined object the null object, so nothing here fails to parse -- ReadContext and Validate both accept a document DanglingRefs reports on, which is exactly why the writer this package uses cannot rely on either to catch the bug byb-yul.6 is about.

func (DanglingRef) String added in v0.3.0

func (d DanglingRef) String() string

type DecodeParms

type DecodeParms struct {
	Predictor        int
	Colors           int
	BitsPerComponent int
	Columns          int
}

DecodeParms is the subset of /DecodeParms Byblos' encoders emit.

A zero Predictor means no predictor, which is also how PDF reads an absent /Predictor (ISO 32000-1 table 10 gives it default 1, "no prediction").

type Doc

type Doc interface {
	PageCount() int
	Page(n int) (*Page, error)
	// Annots returns page n's annotations. They are not part of Page because
	// nothing in classification reads them yet; see annots.go.
	Annots(n int) ([]Annot, error)
	// XObject and ExtGStateOpaque implement content.Env.
	XObject(scope int, name string) (content.XObject, bool)
	ExtGStateOpaque(scope int, name string) bool
	// ImageInfo returns the dictionary facts for an image resolved by XObject,
	// keyed by the ID that XObject returned.
	ImageInfo(id int) (ImageInfo, bool)
	// EncryptInfo reports whether this document carries an /Encrypt
	// dictionary, and its /P, /V, /R when so. See encryptinfo.go.
	EncryptInfo() EncryptInfo
	// RawImage renders an image previously resolved by XObject and returns its
	// bytes and the file type pdfcpu inferred. The id is the one XObject
	// returned; an id this document has not resolved is an error.
	RawImage(id int) (data []byte, fileType string, err error)
	// RawImageGlobals returns the JBIG2 page-0 segments an image's
	// /DecodeParms names in /JBIG2Globals, decoded, and nil when it names
	// none. See the method for why it is separate from RawImage.
	RawImageGlobals(id int) ([]byte, error)
	// ReplaceImage and Write are the write half; see write.go. They are on the
	// same interface because writing requires the context Open normalized, so
	// there is no way to reach them from a document Byblos did not read.
	ReplaceImage(id int, img EncodedImage) error
	Write(w io.Writer) error
	// AddFontResource and AppendContent are the invisible-text write half; see
	// text.go. Same reason as ReplaceImage/Write: they need Open's normalized
	// context.
	AddFontResource(n int, f TrueTypeFont) (name string, err error)
	AppendContent(n int, ops []byte) error
	// WrapContent brackets page n's whole content with before and after, as
	// two new streams; see text.go.
	WrapContent(n int, before, after []byte) error
}

Doc is a parsed PDF and the seam Byblos keeps between itself and pdfcpu.

func Open

func Open(rs io.ReadSeeker) (d Doc, err error)

Open parses rs. It does not run pdfcpu's validator: real scanner output exercises relaxed paths, and rejecting a readable file helps nobody. The validation gate belongs to Optimize (B5) and to the caller's policy.

rs is read once, here. Nothing below re-reads it, so a file this function accepts cannot later be rejected by a validator Byblos opted out of.

type EncodedImage

type EncodedImage struct {
	Width, Height int
	BPC           int // /BitsPerComponent
	ColorSpace    ColorSpace
	Filter        string // e.g. "JBIG2Decode", "FlateDecode", "DCTDecode"
	DecodeParms   *DecodeParms
	Data          []byte
}

EncodedImage is a fully encoded image ready to be stored verbatim.

Data is the ENCODED stream payload: whatever /Filter names has already been applied to it. Nothing here compresses, and nothing here checks that Data actually decodes to Width x Height samples — that is the encoder's contract.

type EncryptInfo added in v0.3.0

type EncryptInfo struct {
	Encrypted bool
	P, V, R   int
}

EncryptInfo is what a document Open already accepted says about its own /Encrypt dictionary.

type FontEncoding added in v0.3.0

type FontEncoding int

FontEncoding is what a font dict says about mapping character codes to glyphs. The three cases are distinguished because they need different amounts of work from a decoder, not because the PDF spec groups them this way.

const (
	// EncodingAbsent is no /Encoding at all, or an encoding dict carrying
	// neither /Differences nor /BaseEncoding. Both need the font program's own
	// built-in encoding, so a dict with neither is no better than none.
	EncodingAbsent FontEncoding = iota
	// EncodingNamed is a bare name such as /WinAnsiEncoding, or an encoding
	// dict with a /BaseEncoding. Either way it is a table lookup.
	EncodingNamed
	// EncodingDifferences is an encoding dict carrying /Differences.
	EncodingDifferences
)

type FontFacts added in v0.3.0

type FontFacts struct {
	Subtype   string
	ToUnicode bool
	Encoding  FontEncoding
	// Symbolic is FontDescriptor /Flags bit 3 set AND bit 6 clear (ISO 32000-1
	// table 123; the spec numbers bits from 1, so those are 1<<2 and 1<<5). A
	// font claiming both is contradictory and is not reported as symbolic.
	Symbolic bool
}

FontFacts is what one /Type /Font dictionary says about itself.

Subtype is reported verbatim, INCLUDING CIDFontType0 and CIDFontType2. Those descendants are never shown directly -- their Type0 parent is -- but whether to exclude them is a counting decision, so it belongs to the caller and not here.

func FontDicts added in v0.3.0

func FontDicts(path string) ([]FontFacts, error)

FontDicts returns the facts for every /Type /Font dict in the file's xref table, or ErrFontCensusUnreadable.

It takes a path rather than an io.ReadSeeker, unlike the rest of this package, because the two reads it needs are api.ReadContextFile and api.ReadContext and only the first accepts a path. That asymmetry is the whole point of the function -- see readBothForFonts.

type ImageInfo

type ImageInfo struct {
	Name          string
	ObjNr         int
	Width, Height int  // pixels
	BPC           int  // /BitsPerComponent; 0 when absent
	ImageMask     bool // /ImageMask: a stencil painted in the fill colour
	SMask         bool // /SMask: a soft mask supplies per-pixel alpha
	Mask          bool // /Mask: a stencil mask or a colour-key range
	Decode        bool // /Decode is present: the samples need a remap

	// DecodeArray is /Decode's elements when every one of them is a number,
	// and nil otherwise — either the entry is absent, or it is present in a
	// shape this cannot read.
	//
	// Decode and this field answer different questions and a caller must pick
	// the one it means. "Is there a remap at all" is Decode, and an entry whose
	// numbers could not be read is still a remap; optimize.go's eligibility
	// test reads it that way. "What is the remap" is this, and nil means there
	// is none to be had — never that the samples pass through unchanged.
	//
	// The values are as written. This does not supply the default array for an
	// absent entry, because the default depends on the colour space and the
	// bit depth (ISO 32000-1 table 39) and ColorSpace is deliberately not
	// resolved here.
	DecodeArray []float64

	// Filter is the image codec the stream declares: /Filter when it is a
	// name, the LAST entry when it is an array, and "" when /Filter is absent
	// (an unencoded stream) or is neither of those shapes.
	//
	// Last, not first, because a filter array is the DECODE order (ISO 32000-1
	// section 7.4): in /Filter [/ASCII85Decode /JBIG2Decode] the ASCII85 stage
	// only unwraps the transport encoding and the image codec is JBIG2Decode.
	// Reading the first entry would report a chained JBIG2 image as ASCII85 and
	// undercount exactly the class this field exists to find.
	//
	// It is a declaration, like everything else here — RawImage still reports
	// what pdfcpu actually made of the bytes. The two disagree when a file lies
	// about its own encoding.
	Filter string

	// ColorSpace is /ColorSpace when it is a plain name ("DeviceGray",
	// "DeviceRGB", "DeviceCMYK"), and "" when it is an array or an indirect
	// reference -- Indexed, ICCBased, Separation, DeviceN. Recompression uses
	// it as an eligibility test, not as colour management: replacing a
	// /Separation with /DeviceGray inverts the ink convention (in Separation,
	// 0 is no ink and therefore white; in DeviceGray, 0 is black), so
	// anything that is not a device name is left alone.
	ColorSpace string
}

ImageInfo describes an image XObject as its own dictionary declares it.

SMask, Mask and ImageMask are the three ways an image can fail to be an opaque rectangle of pixels. They are recorded as presence, not as content: what the mask actually does needs a renderer, and for deciding whether this image hides what is painted under it, "it has one" is the whole answer.

type Page

type Page struct {
	Index    int // 1-based
	MediaBox Rect
	CropBox  Rect // equals MediaBox when the page declares none
	Rotate   int
	Content  []byte // decoded, concatenated
	Scope    int    // resource scope handle for content.Env
	// MediaBoxDefaulted reports that the document declared no /MediaBox
	// anywhere in this page's inheritance chain, so MediaBox is the US Letter
	// convention rather than anything the file states. See Page(), and byb-8ly
	// for the nine govdocs1 files that made it necessary.
	MediaBoxDefaulted bool
}

Page is one page's geometry, content, and resource scope.

type PageSource added in v0.3.0

type PageSource struct {
	// Source is the document to take the page from. Two entries naming the
	// SAME io.ReadSeeker are one logical source, opened once and shared.
	Source io.ReadSeeker
	// Page is 1-based, in Source.
	Page int
	// Rotate is the ABSOLUTE /Rotate to give the page: 0, 90, 180 or 270. It
	// is not added to whatever the source page declares.
	Rotate int
	// Straighten is a lossless rotation of the page's content, nil for none.
	// See StraightenSpec's doc comment for the sign convention and the
	// absolute-not-delta contract (byb-16j.4).
	Straighten *StraightenSpec
}

PageSource names one page of one document, and the rotation to give it.

type Rect

type Rect struct{ LLX, LLY, URX, URY float64 }

Rect is a rectangle in PDF default user space: points, origin lower-left, y increasing upward.

type StraightenSpec added in v0.3.0

type StraightenSpec struct {
	Deg float64
	// Crop is [llx lly urx ury] in the source page's unrotated PDF user
	// space (design spec section 6). It is refused when non-nil: cropping
	// is not implemented in this version, and refusing is what lets the
	// field exist in the contract now without a caller silently getting a
	// page that ignored it.
	Crop *[4]float64
}

StraightenSpec is an absolute correction to apply to one page's content.

Deg is the rotation byblos applies, in degrees, positive COUNTER-CLOCKWISE in PDF default user space. That is the same signed convention as skew.Estimate.Deg (internal/skew/skew.go:70-74), pinned there by TestSignIsUserSpace, and it is stated in exactly one place for both sides of the repository boundary.

It is ABSOLUTE. It is the angle from the ORIGINAL page, never a delta on whatever is already applied. kleio redelivers a job at least once (ocr.go:55-60), so a transform that composes with what is already there corrupts the page on a retry. This is the same argument byb-yul.4 settled for PageSource.Rotate.

ABSOLUTE IS ENFORCED, NOT ASSUMED. This package's BuildFromPages only applies the angle it is given; it has no way to see a document's own provenance. The enforcement lives one layer up, in the root package's BuildFromPagesContext: the rotation it hands down to this package is Deg minus whatever the source page's provenance already records as Straightened.Deg, defaulting to zero, and the record it writes afterwards is always the total (Deg), never the increment actually applied.

To straighten a page flat: measure the raster's content angle D with internal/skew, read the placement angle p from ImageRef.PlacementDeg, and set Deg = -(p + D). The two cancel on a page that already reads straight.

type TrueTypeFont

type TrueTypeFont struct {
	BaseFont    string // /BaseFont and /FontDescriptor /FontName
	Program     []byte // the sfnt bytes; becomes /FontFile2 with /Length1 = len(Program)
	FirstChar   int
	Widths      []int
	Flags       int
	FontBBox    [4]int
	ItalicAngle int
	Ascent      int
	Descent     int
	CapHeight   int
	StemV       int
}

TrueTypeFont describes a simple, single-byte-encoded TrueType font to embed.

It is deliberately narrow: exactly the fields a simple (non-Type0) /FontDescriptor and /Font dictionary need. Widths is in 1000ths of text space (ISO 32000-1 9.2.4), indexed from FirstChar, one entry per character code up to LastChar (== FirstChar+len(Widths)-1).

Jump to

Keyboard shortcuts

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