Skip to content

Set Outlines

A model.Outline built in Go or unmarshaled from JSON becomes a document’s bookmarks through PdfWriter.AddOutlineTree. The shape of the JSON is exactly what get outlines produces, so the usual workflow is to dump the outlines of one document, edit the file, and apply it back.

Doing it

var newOutlines model.Outline
if err := json.Unmarshal(data, &newOutlines); err != nil {
    return err
}

pdfWriter, err := pdfReader.ToWriter(&model.ReaderToWriterOpts{SkipOutlines: true})
if err != nil {
    return err
}

pdfWriter.AddOutlineTree(newOutlines.ToOutlineTree())

return pdfWriter.WriteToFile("output.pdf")

SkipOutlines: true matters. Without it, ToWriter copies the reader’s existing outline tree into the writer, and the AddOutlineTree call then replaces it, so the result is the same but the copy is wasted work; the option states the intent. What you cannot do this way is merge. AddOutlineTree sets the tree, it does not append to it, so adding a bookmark to a document means reading its outlines with GetOutlines first, adding to that Outline, and writing the whole tree back.

ToOutlineTree is what turns the high-level tree into the linked list of PdfOutlineTreeNode a PDF wants. It walks the Entries of every item, wires up First, Last, Next, Prev and Parent, and sets each item’s Count to its number of descendants. Nothing needs to be linked by hand.

Building the tree in code instead of reading JSON uses the same types:

outline := model.NewOutline()
item := model.NewOutlineItem("Chapter 1", model.NewOutlineDest(0, 0, 792))
item.Add(model.NewOutlineItem("Section 1.1", model.NewOutlineDest(0, 50, 640)))
outline.Add(item)

NewOutlineDest(page, x, y) takes a zero-based page index and sets Mode to XYZ. OutlineItem.Add appends a child, Insert puts one at an index, and the same two methods exist on Outline for top level items.

Coordinates are in PDF user space with the origin at the bottom left of the page, so y counts up from the bottom. To point at a particular piece of text, extract its bounding box and use Llx for x and Ury for y.

Limitations

The page value in the JSON is a zero-based index, so the first page is 0. Getting this wrong is the usual cause of bookmarks landing one page off.

Mode decides which coordinates are written out. XYZ writes x, y and zoom; FitH and FitBH write only y; FitV and FitBV only x; Fit and FitB write neither. Anything else is silently replaced with Fit, which discards the coordinates. A destination with an empty mode, or a negative page and no page object, is written as a null destination, giving a bookmark that goes nowhere.

Destinations loaded from JSON carry a page index rather than a reference to the page object, because OutlineDest.PageObj is not serialized. UniPDF writes the index as an integer in the destination array. The same is true of destinations built with NewOutlineDest, so this is the normal output of the API, but it does mean the page index is resolved by position: applying a JSON file to a document whose pages are in a different order points the bookmarks at the wrong pages.

Titles are written as encoded strings, so non-ASCII titles survive. Anything else a PDF bookmark can carry, such as color, bold or italic styling, or an open or closed initial state, is not part of OutlineItem and cannot be set through it.

Run the example

applyOutlines is the whole example: read the JSON, unmarshal into a model.Outline, convert the reader to a writer with SkipOutlines, apply the tree and write the file. It takes three paths, in the order input, outlines, output.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/outlines
go run pdf_set_outlines.go input.pdf outlines.json output.pdf

If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.

View the full source
Last updated on