fix(pdfengines): correctly update the indexes if the bookmarks form field (map format) is given

This commit is contained in:
Julien Neuhart
2026-03-04 22:45:58 +01:00
parent caea81501d
commit 874e78c6cd
12 changed files with 257 additions and 24 deletions

View File

@@ -203,6 +203,37 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
return nil
}
// PageCount returns the number of pages in a PDF file using ExifTool.
func (engine *ExifTool) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
if err != nil {
return 0, fmt.Errorf("read metadata with ExifTool: %w", err)
}
pageCountValue, ok := metadata["PageCount"]
if !ok {
return 0, errors.New("PageCount not found in metadata")
}
switch val := pageCountValue.(type) {
case int:
return val, nil
case int64:
return int(val), nil
case float64:
return int(val), nil
case string:
var res int
_, err := fmt.Sscanf(val, "%d", &res)
if err != nil {
return 0, fmt.Errorf("parse PageCount string '%s': %w", val, err)
}
return res, nil
default:
return 0, fmt.Errorf("unexpected PageCount type '%T'", pageCountValue)
}
}
// WriteBookmarks is not available in this implementation.
func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)

View File

@@ -91,6 +91,11 @@ func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *z
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// PageCount is not available in this implementation.
func (engine *LibreOfficePdfEngine) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// WriteBookmarks is not available in this implementation.
func (engine *LibreOfficePdfEngine) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)

View File

@@ -183,6 +183,11 @@ func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, met
return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// PageCount is not available in this implementation.
func (engine *PdfCpu) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// ReadBookmarks reads the document outline (bookmarks) of a PDF file using pdfcpu.
func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
tmpPath := fmt.Sprintf("%s.read.json", inputPath)

View File

@@ -222,6 +222,42 @@ func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Log
return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
}
type pageCountResult struct {
pageCount int
err error
}
// PageCount returns the number of pages in a PDF file using the first available
// engine that supports metadata reading.
func (multi *multiPdfEngines) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
var err error
var mu sync.Mutex // to safely append errors.
for _, engine := range multi.readMetadataEngines {
resultChan := make(chan pageCountResult, 1)
go func(engine gotenberg.PdfEngine) {
pageCount, err := engine.PageCount(ctx, logger, inputPath)
resultChan <- pageCountResult{pageCount: pageCount, err: err}
}(engine)
select {
case result := <-resultChan:
if result.err != nil {
mu.Lock()
err = multierr.Append(err, result.err)
mu.Unlock()
} else {
return result.pageCount, nil
}
case <-ctx.Done():
return 0, ctx.Err()
}
}
return 0, fmt.Errorf("page count with multi PDF engines: %w", err)
}
type readBookmarksResult struct {
bookmarks []gotenberg.Bookmark
err error

View File

@@ -292,6 +292,21 @@ func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata ma
return nil
}
func shiftBookmarks(bookmarks []gotenberg.Bookmark, offset int) []gotenberg.Bookmark {
if offset == 0 {
return bookmarks
}
shifted := make([]gotenberg.Bookmark, len(bookmarks))
for i, b := range bookmarks {
shifted[i] = gotenberg.Bookmark{
Title: b.Title,
Page: b.Page + offset,
Children: shiftBookmarks(b.Children, offset),
}
}
return shifted
}
// WriteBookmarksStub writes the bookmarks into PDF files. If no bookmarks, it
// does nothing.
func WriteBookmarksStub(ctx *api.Context, engine gotenberg.PdfEngine, bookmarks any, inputPaths []string) error {
@@ -402,13 +417,6 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err)
}
if b, ok := bookmarks.(map[string][]gotenberg.Bookmark); ok {
err = WriteBookmarksStub(ctx, engine, b, inputPaths)
if err != nil {
return fmt.Errorf("write bookmarks: %w", err)
}
}
outputPath := ctx.GeneratePath(".pdf")
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
if err != nil {
@@ -425,8 +433,27 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("embed files into PDFs: %w", err)
}
var finalBookmarks []gotenberg.Bookmark
if b, ok := bookmarks.([]gotenberg.Bookmark); ok {
err = WriteBookmarksStub(ctx, engine, b, outputPaths)
finalBookmarks = b
} else if b, ok := bookmarks.(map[string][]gotenberg.Bookmark); ok {
offset := 0
for _, inputPath := range inputPaths {
filename := filepath.Base(inputPath)
if fileBookmarks, ok := b[filename]; ok {
finalBookmarks = append(finalBookmarks, shiftBookmarks(fileBookmarks, offset)...)
}
pageCount, err := engine.PageCount(ctx, ctx.Log(), inputPath)
if err != nil {
return fmt.Errorf("get page count of '%s': %w", filename, err)
}
offset += pageCount
}
}
if len(finalBookmarks) > 0 {
err = WriteBookmarksStub(ctx, engine, finalBookmarks, outputPaths)
if err != nil {
return fmt.Errorf("write bookmarks: %w", err)
}

View File

@@ -145,6 +145,11 @@ func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, meta
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// PageCount is not available in this implementation.
func (engine *PdfTk) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// WriteBookmarks is not available in this implementation.
func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)

View File

@@ -172,6 +172,11 @@ func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metad
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// PageCount is not available in this implementation.
func (engine *QPdf) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
return 0, fmt.Errorf("page count with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// WriteBookmarks is not available in this implementation.
func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
return fmt.Errorf("write PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)