Skip to content
v4 to v5 Migration Guide

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: ICCBased colorspaces 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.
These are behavioral changes. Rendered output that depends on color conversion may change slightly compared to v4.

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.

RemovedReplacement
model.NewCompositePdfFontFromTTFFilemodel.NewCompositePdfFontFile
model.NewCompositePdfFontFromOTFFilemodel.NewCompositePdfFontFile
model.NewCompositePdfFontFromTTFmodel.NewCompositePdfFont
model.NewCompositePdfFontFromOTFmodel.NewCompositePdfFont
(*PdfFont).CharcodesToUnicodeWithStats(*PdfFont).CharcodesToUnicode
(PageText).ToText(PageText).Text
(*contentstream.ContentStreamParser).ExtractTextextractor package
model.PageCallback (and the PageCallback reader option)removed, no replacement

Other Improvements

  • The PdfObject interface gains Direct() and Equals() methods, removing the need for repetitive type switches. This is a breaking change for external PdfObject implementers: 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 CIDFontType2 fonts with a non-identity CIDToGIDMap, 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/v5

Then 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 working

You 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.ReadSeeker type that does not implement io.ReaderAt.
  • You accessed PdfParser.ObjCache directly.
  • 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.

OldNewExplicit-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
OldNew
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 a sync.Map object cache and internal mutexes.
  • *model.PdfReader: page access (GetPage) and content traversal across goroutines, including readers built via NewPdfReaderFromParser.
  • Signature verification reads via parser.ReadBytesAt rather 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 via Size(), Stat(), or Seek().
  • parser.ParseIndirectObject(): wrapper for ParseIndirectObjectAt(0).
  • parser.ParseDict(): wrapper for ParseDictAt(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.

Last updated on