Skip to content

Generate a TOC via OLE

The native DOCX to PDF converter does not evaluate TOC fields. It handles PAGE, NUMPAGES, REF and FORMCHECKBOX, and nothing else, so a document whose contents page is a TOC field converts to a PDF with an empty contents page. No error, no warning. This page is the way around that on Windows.

The reason sits one level down: a table of contents in OOXML is a field instruction, not a list of paragraphs, and UniOffice writes the instruction only. Add a table of contents covers what actually goes into the file.

What you doWhat the contents page contains
Convert with convert.ConvertToPdfNothing. The field code produces no text.
Open the DOCX in Word by handWord computes the entries, because SetUpdateFieldsOnOpen(true) asks it to.
Open and re-save it through OLEThe same computation, unattended, written back into the DOCX.
Export through OLE to PDFWord computes the entries and produces the PDF in one step. See Export to PDF through Word.

The third row is what the example does, and it is the useful one if you want the rest of your pipeline to stay native: Word is used once to fill in the fields, and the resulting DOCX converts correctly from then on.

Writing the field

doc := document.New()
defer doc.Close()

// Ask whatever opens this file to recalculate its fields.
doc.Settings.SetUpdateFieldsOnOpen(true)

doc.AddParagraph().AddRun().AddField(document.FieldTOC)
doc.AddParagraph().Properties().AddSection(wml.ST_SectionMarkNextPage)

para := doc.AddParagraph()
para.Properties().SetHeadingLevel(1)
para.AddRun().AddText("First Level")

doc.SaveToFile("toc.docx")

AddField marks the field dirty, which is the flag Word reads as “this needs recomputing”. Without a heading level on the paragraphs there is nothing for the field to collect: SetHeadingLevel is what puts a paragraph in the contents.

Having Word fill it in

docs := oleutil.MustGetProperty(word, "Documents").ToIDispatch()
wordDoc := oleutil.MustCallMethod(docs, "Open", source).ToIDispatch()

const wdFormatXMLDocument = 12
oleutil.MustCallMethod(wordDoc, "SaveAs2", source, wdFormatXMLDocument)
oleutil.MustCallMethod(wordDoc, "Close")
oleutil.MustCallMethod(word, "Quit")

Note the format constant and the destination. This saves DOCX over DOCX, not PDF: the point is to update the file in place, with the entries and page numbers Word computed on open now stored as ordinary text runs. Converting that file natively afterwards renders them like any other paragraph, since the converter only suppresses the cached result of the PAGE and NUMPAGES fields it computes itself. Use wdFormatPDF, which is 17, if you want the PDF straight out of Word instead.

Limitations

Windows with Word installed, same as any OLE path. The example carries no build tag, so it compiles elsewhere and then fails at CreateObject with a “not implemented” error from go-ole, which it logs before exiting successfully. Check for the updated file, not the exit code.

Page numbers in the result are Word’s pagination, not UniOffice’s. If the native converter lays the document out differently, which font substitution alone can cause, the numbers baked into the contents page will point at the wrong pages of the PDF. Registering the document’s real fonts before converting is what keeps the two in agreement. See Use custom fonts.

The update is a one-off. Edit the document again and the contents page is stale until Word sees it again.

Run the example

The example builds a document with a TOC field, a page break, and four top-level sections of numbered headings nested three levels deep, with filler text between them, saves it as toc.docx, then calls UpdateFields to round-trip it through Word. There is no PDF in the output; toc.docx with a populated contents page is the result.

git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/toc-generation-ole
go run main.go

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

View the full source
Last updated on