v4 to v5 Migration Guide
UniPDF v5 is the next major release of the library. It focuses on a more robust type system, concurrent document access, accessibility (PDF/UA), color accuracy (ICC), and Fast Web View (linearization), while cleaning up several long-deprecated APIs. Release artifacts are published on the UniPDF releases page.
Most changes are additive and require no code updates. The one change that
affects almost every integration is the parser/reader I/O migration from
io.ReadSeeker to io.ReaderAt; in practice, code that passes a concrete
*os.File or *bytes.Reader keeps working unchanged. This guide walks through
every breaking change with before/after examples, and highlights the new
capabilities you can adopt.
New Features
1. Concurrent page access
The parser and reader I/O layer was migrated from io.ReadSeeker to
io.ReaderAt. This makes the parser internally thread-safe and enables
concurrent page access on a single document, so high-throughput workloads can
read pages from multiple goroutines after loading a document once. A new
shared-parser constructor, model.NewPdfReaderFromParser, lets one parser back
multiple readers. See Parser/reader I/O layer
and Concurrent reading below.
2. PDF/UA (accessibility) support
v5 introduces a new model/pdfua package for accessible (Tagged) PDF: structure
tree construction, role mapping, and validation/repair for both PDF/UA-1
(ISO 14289-1) and PDF/UA-2 (ISO 14289-2). This complements the existing
PDF/A support in model/pdfa. The creator now emits tagged content: figures
with alternate text and bounding boxes, table header cells scoped as TH, and
header/footer blocks marked as pagination artifacts.
3. ICC color support
Two related improvements land in v5.
- ICC colorspace support:
ICCBasedcolorspaces are now honored during rendering, with an internal ICC transform for matrix/TRC profiles. - ICC profile preservation: embedded image ICC profiles are preserved when images are re-embedded, rather than being dropped.
4. PDF linearization (Fast Web View)
v5 can write linearized (“Fast Web View”) PDFs, recognized as such by Adobe Acrobat, including support for object streams and encrypted output. A linearization validator and a PDF/A linearization verifier are included.
import "github.com/unidoc/unipdf/v5/model/optimize"
// After building a *model.PdfWriter:
optimize.Linearize(w)
// or with options:
optimize.LinearizeWithOptions(w, model.LinearizationOptions{ /* ... */ })5. StyledParagraph color gradients
StyledParagraph now supports color gradient fills for text.
6. Component introspection accessors
To make testing and programmatic inspection easier, StyledParagraph, Grid,
GridCell, Table, and TableCell now expose selective Get* accessors for
their contents and fields.
Deprecated and Removed Components
Paragraph component removed
The Paragraph component (deprecated in v4 in favor of StyledParagraph) has
been removed in v5, along with Creator.NewParagraph. Migrate to
StyledParagraph: see Replace the removed Paragraph component
below.
Obsolete functions removed
The following long-deprecated functions were removed. Each has a direct replacement: see Removed functions below.
| Removed | Replacement |
|---|---|
model.NewCompositePdfFontFromTTFFile | model.NewCompositePdfFontFile |
model.NewCompositePdfFontFromOTFFile | model.NewCompositePdfFontFile |
model.NewCompositePdfFontFromTTF | model.NewCompositePdfFont |
model.NewCompositePdfFontFromOTF | model.NewCompositePdfFont |
(*PdfFont).CharcodesToUnicodeWithStats | (*PdfFont).CharcodesToUnicode |
(PageText).ToText | (PageText).Text |
(*contentstream.ContentStreamParser).ExtractText | extractor package |
model.PageCallback (and the PageCallback reader option) | removed, no replacement |
Other Improvements
- The
PdfObjectinterface gainsDirect()andEquals()methods, removing the need for repetitive type switches. This is a breaking change for externalPdfObjectimplementers: see PdfObject interface additions below. - Charcode / GID / rune separation: text encoding internals now keep
character codes, glyph indices, and runes as distinct types, fixing a class of
rendering bugs (notably
CIDFontType2fonts with a non-identityCIDToGIDMap, and notdef/out-of-range CIDs on the Identity render path). - XMP metadata now uses a maintained fork of the XMP library.
- Radial shading fills no longer clip to the disc, and stale linearization dictionaries are neutralized on signing.
Migration Guide
Installation
Update the import path from v4 to v5.
go get github.com/unidoc/unipdf/v5Then update your imports.
// Before
import "github.com/unidoc/unipdf/v4/model"
// After
import "github.com/unidoc/unipdf/v5/model"Breaking Changes and Deprecated APIs
1. Parser/reader I/O layer: io.ReadSeeker -> io.ReaderAt
This is the most widely reaching change. The parser and reader were migrated
from io.ReadSeeker to io.ReaderAt to enable concurrent access and make the
parser internally thread-safe.
Does my code break?
If you construct a reader from a concrete *os.File, *bytes.Reader, or
*io.SectionReader, no change is needed. All three satisfy both
io.ReadSeeker and io.ReaderAt, and the new constructors auto-detect the
document size.
f, _ := os.Open("doc.pdf")
defer f.Close()
reader, err := model.NewPdfReader(f) // unchanged, keeps workingYou do need to adapt if any of the following apply.
- You passed a variable typed as
io.ReadSeeker(the interface) to a constructor. - You used a custom
io.ReadSeekertype that does not implementio.ReaderAt. - You accessed
PdfParser.ObjCachedirectly. - You called
PdfParser.GetPreviousRevisionReadSeeker(). - You relied on
parser.ParseDict()/parser.ParseIndirectObject()reading from a non-zero file offset set by a previous parser operation.
Constructor signature changes
The size argument is auto-detected via Size()/Stat()/Seek(). An
explicit-size *At variant exists for sources that expose none of those.
| Old | New | Explicit-size escape hatch |
|---|---|---|
core.NewParser(rs io.ReadSeeker) | core.NewParser(r io.ReaderAt) | core.NewParserAt(r, size) |
core.NewParserWithOpts(rs, opts) | core.NewParserWithOpts(r io.ReaderAt, opts) | core.NewParserWithOptsAt(r, size, opts) |
core.NewCompliancePdfParser(rs) | core.NewCompliancePdfParser(r io.ReaderAt) | core.NewCompliancePdfParserAt(r, size) |
model.NewPdfReader(rs) | model.NewPdfReader(r io.ReaderAt) | model.NewPdfReaderAt(r, size) |
model.NewPdfReaderLazy(rs) | model.NewPdfReaderLazy(r io.ReaderAt) | model.NewPdfReaderLazyAt(r, size) |
model.NewPdfReaderWithOpts(rs, opts) | model.NewPdfReaderWithOpts(r io.ReaderAt, opts) | model.NewPdfReaderWithOptsAt(r, size, opts) |
model.NewCompliancePdfReader(rs) | model.NewCompliancePdfReader(r io.ReaderAt) | model.NewCompliancePdfReaderAt(r, size) |
If you pass a variable typed as io.ReadSeeker
Type-assert to io.ReaderAt.
// Before
func openPdfReader(rs io.ReadSeeker) (*model.PdfReader, error) {
return model.NewPdfReader(rs)
}
// After
func openPdfReader(rs io.ReadSeeker) (*model.PdfReader, error) {
ra, ok := rs.(io.ReaderAt)
if !ok {
return nil, errors.New("source does not implement io.ReaderAt")
}
return model.NewPdfReader(ra)
}Every io.ReadSeeker from the standard library (*os.File, *bytes.Reader,
*io.SectionReader, *strings.Reader) also implements io.ReaderAt, so the
assertion succeeds for typical callers.
If your source is a custom io.ReadSeeker without ReadAt
Buffer it into a *bytes.Reader first.
buf, err := io.ReadAll(myReadSeeker)
if err != nil {
return err
}
reader, err := model.NewPdfReader(bytes.NewReader(buf))Renamed method: GetPreviousRevisionReadSeeker
PdfParser.GetPreviousRevisionReadSeeker() (io.ReadSeeker, error) was renamed
to GetPreviousRevision() (*io.SectionReader, error). The concrete
*io.SectionReader satisfies both io.ReadSeeker and io.ReaderAt, so feed it
directly into NewParser / NewPdfReader.
// Before
rs, err := parser.GetPreviousRevisionReadSeeker()
// After
sec, err := parser.GetPreviousRevision()Removed exported field: PdfParser.ObjCache
The exported ObjCache field (map[int]PdfObject) was replaced by an
unexported sync.Map for concurrent safety. Use the new accessor methods.
// Before
if obj, ok := parser.ObjCache[objNum]; ok { ... }
parser.ObjCache[objNum] = obj
for k, v := range parser.ObjCache { ... }
// After
if obj, ok := parser.LoadCachedObject(objNum); ok { ... }
parser.StoreCachedObject(objNum, obj)
parser.RangeCachedObjects(func(k int, v core.PdfObject) bool {
// ...
return true // continue
})Behavior change: ParseDict() / ParseIndirectObject() parse from offset 0
Previously, these read from the parser’s internal shared file cursor. They now always start at byte 0. Use the new offset-aware variants for a specific offset.
// Before, implicitly used wherever the parser cursor was positioned
dict, err := parser.ParseDict()
// After, explicit offset
dict, err := parser.ParseDictAt(offset)
// or, equivalently for offset 0:
dict, err := parser.ParseDict()The same applies to ParseIndirectObject() / ParseIndirectObjectAt(offset).
Behavior change: Appender requires both io.ReadSeeker and io.ReaderAt
model.NewPdfAppender and NewPdfAppenderWithOpts now return an error if the
reader’s source does not satisfy both interfaces. Every seekable source you
would append to (*os.File, *bytes.Reader) does. Readers built via
NewPdfReaderFromParser cannot be appended.
Behavior change: fjson.LoadFromPDF and pdfutil.MergePdfStreams* take io.ReaderAt
| Old | New |
|---|---|
fjson.LoadFromPDF(rs io.ReadSeeker) | fjson.LoadFromPDF(r io.ReaderAt) |
pdfutil.MergePdfStreams(inputs []io.ReadSeeker, ...) | pdfutil.MergePdfStreams(inputs []io.ReaderAt, ...) |
pdfutil.MergePdfStreamsWithOptions(inputs []io.ReadSeeker, ...) | pdfutil.MergePdfStreamsWithOptions(inputs []io.ReaderAt, ...) |
Concrete-typed callers need no change. Interface-typed callers should
type-assert to io.ReaderAt or buffer via bytes.NewReader.
2. Typed PdfColor interface
In v4, PdfColor was declared as an empty interface
(type PdfColor interface{}), which accepted any value and defeated
compile-time type checking. In v5 it is a typed interface.
// v5
type PdfColor interface {
// Components returns the color's component values in its own colorspace.
Components() []float64
// Colorspace returns the colorspace family this color belongs to.
Colorspace() PdfColorspace
}All built-in color types (PdfColorDeviceGray, PdfColorDeviceRGB,
PdfColorDeviceCMYK, PdfColorCalGray, PdfColorCalRGB, and others) already
satisfy this interface, so internal callers are unaffected. Third-party code
that assigned arbitrary values to a PdfColor field will no longer compile
and must provide a type implementing Components() and Colorspace().
3. PdfObject interface additions
The core.PdfObject interface gains two methods.
type PdfObject interface {
String() string
Write() []byte
Direct() PdfObject // new: equivalent to TraceToDirectObject(obj)
Equals(other PdfObject) bool // new: equivalent to EqualObjects(obj, other)
}Built-in object types implement these. If you have a custom type that
implements core.PdfObject, add Direct() and Equals() to keep it
compiling.
// Direct returns the direct object this represents; leaf/container objects
// return themselves.
func (o *MyObject) Direct() core.PdfObject { return o }
// Equals reports whether other has the same contents as this object.
func (o *MyObject) Equals(other core.PdfObject) bool {
return core.EqualObjects(o, other)
}Call sites can now replace core.TraceToDirectObject(obj) with obj.Direct(),
and manual comparison switches with obj.Equals(other).
4. Replace the removed Paragraph component
Paragraph and Creator.NewParagraph are removed. Use StyledParagraph.
// Before (v4, deprecated)
p := c.NewParagraph("Hello world")
p.SetFontSize(12)
c.Draw(p)
// After (v5)
p := c.NewStyledParagraph()
p.SetText("Hello world")
p.SetFontSize(12)
c.Draw(p)5. Removed functions (with direct replacements)
// Composite font constructors, before
font, _ := model.NewCompositePdfFontFromTTFFile("font.ttf")
font, _ := model.NewCompositePdfFontFromOTFFile("font.otf")
font, _ := model.NewCompositePdfFontFromTTF(r)
font, _ := model.NewCompositePdfFontFromOTF(r)
// After, use the unified constructors
font, _ := model.NewCompositePdfFontFile("font.ttf") // .ttf or .otf
font, _ := model.NewCompositePdfFont(r) // io.ReadSeeker// PageText.ToText, before
s := pageText.ToText()
// After
s := pageText.Text()// PdfFont.CharcodesToUnicodeWithStats, before
runes, hits, misses := font.CharcodesToUnicodeWithStats(charcodes)
// After, CharcodesToUnicode returns the runes; drop the stats
runes := font.CharcodesToUnicode(charcodes)(*contentstream.ContentStreamParser).ExtractText is removed. Use the
extractor package for text extraction.
model.PageCallback and the PageCallback reader option are removed with no
replacement.
New APIs and Recommended Patterns
Concurrent reading
The supported concurrent pattern is load once, then read in parallel. Build the reader, which resolves the document structure up front, then access pages from multiple goroutines.
data, _ := os.ReadFile("doc.pdf")
parser, _ := core.NewParser(bytes.NewReader(data))
reader, _ := model.NewPdfReaderFromParser(parser) // resolves structure eagerly
// reader is now safe to use concurrently:
var wg sync.WaitGroup
for i := 1; i <= numPages; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
page, _ := reader.GetPage(n)
// ... process page
}(i)
}
wg.Wait()What is safe once the structure is loaded:
*core.PdfParser: cached object lookups (LookupByReference,LookupByNumber,Resolve), backed by async.Mapobject cache and internal mutexes.*model.PdfReader: page access (GetPage) and content traversal across goroutines, including readers built viaNewPdfReaderFromParser.- Signature verification reads via
parser.ReadBytesAtrather than mutating a shared cursor, so it no longer interferes with concurrent reads.
NewPdfReaderFromParser does not support encrypted documents, and the reader it
returns has no io.ReadSeeker source so it cannot be passed to
NewPdfAppender. Reading and verifying existing signatures does work, because
the parser reads raw bytes through ReadBytesAt rather than a shared cursor.
The original single-threaded pattern (one reader, one goroutine) is unchanged
and remains the simplest, most efficient choice for most workloads.Additional helpers:
core.SizeOfReaderAt(r io.ReaderAt) (int64, error): derives a source’s byte length viaSize(),Stat(), orSeek().parser.ParseIndirectObject(): wrapper forParseIndirectObjectAt(0).parser.ParseDict(): wrapper forParseDictAt(0).parser.ReadBytesAt(offset, length): stateless read at an absolute offset.
See also the concurrency guides.
Writing linearized (Fast Web View) PDFs
import "github.com/unidoc/unipdf/v5/model/optimize"
optimize.Linearize(w) // w is a *model.PdfWriter
// or:
optimize.LinearizeWithOptions(w, model.LinearizationOptions{ /* ... */ })Object streams (UseObjectStreams=true) and encrypted output are supported on
the linearized path.
Accessible PDFs (PDF/UA)
Use the model/pdfua package to validate and repair Tagged PDF for both
PDF/UA-1 (ISO 14289-1) and PDF/UA-2 (ISO 14289-2). The creator emits tagged
content automatically when tagging is enabled. See the
accessibility guides.
Component introspection
StyledParagraph, Grid, GridCell, Table, and TableCell expose Get*
accessors for reading back their contents and configured fields, which is
useful for tests and programmatic inspection.