diff --git a/creator/creator.go b/creator/creator.go index 7903c39e..a886c3d2 100644 --- a/creator/creator.go +++ b/creator/creator.go @@ -13,6 +13,7 @@ import ( "strconv" "github.com/unidoc/unipdf/v3/common" + "github.com/unidoc/unipdf/v3/core" "github.com/unidoc/unipdf/v3/model" ) @@ -62,6 +63,10 @@ type Creator struct { // Forms. acroForm *model.PdfAcroForm + // Page labels. + pageLabels core.PdfObject + + // Optimizer. optimizer model.Optimizer // Default fonts used by all components instantiated through the creator. @@ -82,6 +87,14 @@ func (c *Creator) SetOutlineTree(outlineTree *model.PdfOutlineTreeNode) { c.externalOutline = outlineTree } +// SetPageLabels adds the specified page labels to the PDF file generated +// by the creator. See section 12.4.2 "Page Labels" (p. 382 PDF32000_2008). +// NOTE: for existing PDF files, the page label ranges object can be obtained +// using the model.PDFReader's GetPageLabels method. +func (c *Creator) SetPageLabels(pageLabels core.PdfObject) { + c.pageLabels = pageLabels +} + // FrontpageFunctionArgs holds the input arguments to a front page drawing function. // It is designed as a struct, so additional parameters can be added in the future with backwards // compatibility. @@ -647,6 +660,14 @@ func (c *Creator) Write(ws io.Writer) error { pdfWriter.AddOutlineTree(&c.outline.ToPdfOutline().PdfOutlineTreeNode) } + // Page labels. + if c.pageLabels != nil { + if err := pdfWriter.SetPageLabels(c.pageLabels); err != nil { + common.Log.Debug("ERROR: Could not set page labels: %v", err) + return err + } + } + // Pdf Writer access hook. Can be used to encrypt, etc. via the PdfWriter instance. if c.pdfWriterAccessFunc != nil { err := c.pdfWriterAccessFunc(&pdfWriter) diff --git a/creator/creator_test.go b/creator/creator_test.go index b5818d4a..b59c4ffe 100644 --- a/creator/creator_test.go +++ b/creator/creator_test.go @@ -2980,6 +2980,60 @@ func TestCreatorStable(t *testing.T) { } } +func TestPageLabels(t *testing.T) { + // Read input file. + f, err := os.Open(testPdfTemplatesFile1) + require.NoError(t, err) + defer f.Close() + + reader, err := model.NewPdfReader(f) + require.NoError(t, err) + numPages, err := reader.GetNumPages() + require.NoError(t, err) + + // Add input file pages to a new creator instance. + c := New() + nums := core.MakeArray() + for i := 0; i < numPages; i++ { + page, err := reader.GetPage(i + 1) + require.NoError(t, err) + + err = c.AddPage(page) + require.NoError(t, err) + + // Generate a page range for each page. + // If page index is even, show page label using Roman uppercase numerals. + // Otherwise, show page label using decimal Arabic numerals. + labelStyle := "R" + if i%2 != 0 { + labelStyle = "D" + } + pageRange := core.MakeDict() + pageRange.Set(*core.MakeName("S"), core.MakeName(labelStyle)) + nums.Append(core.MakeInteger(int64(i))) + nums.Append(pageRange) + } + + // Create page labels dictionary and add it to the creator. + genPageLabels := core.MakeDict() + genPageLabels.Set(*core.MakeName("Nums"), nums) + c.SetPageLabels(genPageLabels) + + // Write output file to buffer. + outBuf := bytes.NewBuffer(nil) + err = c.Write(outBuf) + require.NoError(t, err) + + // Read output file. + reader, err = model.NewPdfReader(bytes.NewReader(outBuf.Bytes())) + require.NoError(t, err) + + // Retrieve page labels and compare them to the generated page labels. + pageLabels, err := reader.GetPageLabels() + require.NoError(t, err) + require.Equal(t, core.EqualObjects(genPageLabels, pageLabels), true) +} + var errRenderNotSupported = errors.New("rendering pdf is not supported on this system") // renderPDFToPNGs uses ghostscript (gs) to render specified PDF file into a set of PNG images (one per page). diff --git a/model/reader.go b/model/reader.go index b4348c7d..77a35d49 100644 --- a/model/reader.go +++ b/model/reader.go @@ -765,6 +765,25 @@ func (r *PdfReader) GetNamedDestinations() (core.PdfObject, error) { return obj, nil } +// GetPageLabels returns the PageLabels entry in the PDF catalog. +// See section 12.4.2 "Page Labels" (p. 382 PDF32000_2008). +func (r *PdfReader) GetPageLabels() (core.PdfObject, error) { + obj := core.ResolveReference(r.catalog.Get("PageLabels")) + if obj == nil { + return nil, nil + } + + // Resolve references. + if !r.isLazy { + err := r.traverseObjectData(obj) + if err != nil { + return nil, err + } + } + + return obj, nil +} + // Inspect inspects the object types, subtypes and content in the PDF file returning a map of // object type to number of instances of each. func (r *PdfReader) Inspect() (map[string]int, error) { diff --git a/model/writer.go b/model/writer.go index 8ffbb2e2..1ab1c309 100644 --- a/model/writer.go +++ b/model/writer.go @@ -426,6 +426,18 @@ func (w *PdfWriter) SetNamedDestinations(names core.PdfObject) error { return w.addObjects(names) } +// SetPageLabels sets the PageLabels entry in the PDF catalog. +// See section 12.4.2 "Page Labels" (p. 382 PDF32000_2008). +func (w *PdfWriter) SetPageLabels(pageLabels core.PdfObject) error { + if pageLabels == nil { + return nil + } + + common.Log.Trace("Setting catalog PageLabels...") + w.catalog.Set("PageLabels", pageLabels) + return w.addObjects(pageLabels) +} + // SetOptimizer sets the optimizer to optimize PDF before writing. func (w *PdfWriter) SetOptimizer(optimizer Optimizer) { w.optimizer = optimizer