test(integration): add bookmarks routes

This commit is contained in:
Julien Neuhart
2026-03-04 21:34:16 +01:00
parent 4296474889
commit 7af3cd1ff5
7 changed files with 415 additions and 50 deletions

View File

@@ -1,6 +1,7 @@
package pdfcpu
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -191,38 +192,60 @@ func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *zap.Logger, inp
return nil, fmt.Errorf("create command: %w", err)
}
_, err = cmd.Exec()
if err != nil {
// pdfcpu returns an error if there are no bookmarks.
// We should check if the error is "no bookmarks available".
// For now, we assume an error means no bookmarks or a real error.
// However, let's try to be a bit more robust if possible.
// If the file does not exist, it's likely there were no bookmarks.
_, statErr := os.Stat(tmpPath)
if os.IsNotExist(statErr) {
return nil, nil
}
return nil, fmt.Errorf("read bookmarks with pdfcpu: %w", err)
}
defer func() {
err := os.Remove(tmpPath)
if err != nil {
if err != nil && !os.IsNotExist(err) {
logger.Error(fmt.Sprintf("remove temporary bookmarks JSON file: %v", err))
}
}()
_, cmdErr := cmd.Exec()
// Check file existence and size.
info, statErr := os.Stat(tmpPath)
if cmdErr != nil {
// If the file wasn't created, or it was created but is 0 bytes,
// it means pdfcpu had no bookmarks to write.
if os.IsNotExist(statErr) || (statErr == nil && info.Size() == 0) {
return make([]gotenberg.Bookmark, 0), nil
}
// Fallback: Check the error string just in case pdfcpu failed without
// touching the file.
if strings.Contains(strings.ToLower(cmdErr.Error()), "no bookmarks") {
return make([]gotenberg.Bookmark, 0), nil
}
return nil, fmt.Errorf("read bookmarks with pdfcpu: %w", cmdErr)
}
// If cmd succeeded, but output a 0-byte file anyway.
if info != nil && info.Size() == 0 {
return make([]gotenberg.Bookmark, 0), nil
}
// Read the file content.
jsonBytes, err := os.ReadFile(tmpPath)
if err != nil {
return nil, fmt.Errorf("read temporary bookmarks JSON file: %w", err)
}
// Check if the content is just empty whitespace.
if len(bytes.TrimSpace(jsonBytes)) == 0 {
return make([]gotenberg.Bookmark, 0), nil
}
var data pdfcpuBookmarks
err = json.Unmarshal(jsonBytes, &data)
if err != nil {
return nil, fmt.Errorf("unmarshal bookmarks: %w", err)
}
// Safety check: Does the parsed JSON actually contain bookmarks?
if len(data.Bookmarks) == 0 {
return make([]gotenberg.Bookmark, 0), nil
}
var mapBookmarks func(bookmarks []pdfcpuBookmark) []gotenberg.Bookmark
mapBookmarks = func(bookmarks []pdfcpuBookmark) []gotenberg.Bookmark {
res := make([]gotenberg.Bookmark, len(bookmarks))

View File

@@ -12,16 +12,16 @@ import (
)
type multiPdfEngines struct {
mergeEngines []gotenberg.PdfEngine
splitEngines []gotenberg.PdfEngine
flattenEngines []gotenberg.PdfEngine
convertEngines []gotenberg.PdfEngine
readMetadataEngines []gotenberg.PdfEngine
writeMetadataEngines []gotenberg.PdfEngine
passwordEngines []gotenberg.PdfEngine
embedEngines []gotenberg.PdfEngine
bookmarksEngines []gotenberg.PdfEngine
readBookmarksEngines []gotenberg.PdfEngine
mergeEngines []gotenberg.PdfEngine
splitEngines []gotenberg.PdfEngine
flattenEngines []gotenberg.PdfEngine
convertEngines []gotenberg.PdfEngine
readMetadataEngines []gotenberg.PdfEngine
writeMetadataEngines []gotenberg.PdfEngine
passwordEngines []gotenberg.PdfEngine
embedEngines []gotenberg.PdfEngine
readBookmarksEngines []gotenberg.PdfEngine
writeBookmarksEngines []gotenberg.PdfEngine
}
func newMultiPdfEngines(
@@ -33,20 +33,20 @@ func newMultiPdfEngines(
writeMetadataEngines,
passwordEngines,
embedEngines,
bookmarksEngines,
readBookmarksEngines []gotenberg.PdfEngine,
readBookmarksEngines,
writeBookmarksEngines []gotenberg.PdfEngine,
) *multiPdfEngines {
return &multiPdfEngines{
mergeEngines: mergeEngines,
splitEngines: splitEngines,
flattenEngines: flattenEngines,
convertEngines: convertEngines,
readMetadataEngines: readMetadataEngines,
writeMetadataEngines: writeMetadataEngines,
passwordEngines: passwordEngines,
embedEngines: embedEngines,
bookmarksEngines: bookmarksEngines,
readBookmarksEngines: readBookmarksEngines,
mergeEngines: mergeEngines,
splitEngines: splitEngines,
flattenEngines: flattenEngines,
convertEngines: convertEngines,
readMetadataEngines: readMetadataEngines,
writeMetadataEngines: writeMetadataEngines,
passwordEngines: passwordEngines,
embedEngines: embedEngines,
readBookmarksEngines: readBookmarksEngines,
writeBookmarksEngines: writeBookmarksEngines,
}
}
@@ -264,7 +264,7 @@ func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logger *zap.Lo
var err error
errChan := make(chan error, 1)
for _, engine := range multi.bookmarksEngines {
for _, engine := range multi.writeBookmarksEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.WriteBookmarks(ctx, logger, inputPath, bookmarks)
}(engine)

View File

@@ -36,8 +36,8 @@ type PdfEngines struct {
writeMetadataNames []string
encryptNames []string
embedNames []string
writeBookmarksNames []string
readBookmarksNames []string
writeBookmarksNames []string
engines []gotenberg.PdfEngine
disableRoutes bool
}
@@ -56,8 +56,8 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
fs.StringSlice("pdfengines-encrypt-engines", []string{"qpdf", "pdftk", "pdfcpu"}, "Set the PDF engines and their order for the password protection feature - empty means all")
fs.StringSlice("pdfengines-embed-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the file embedding feature - empty means all")
fs.StringSlice("pdfengines-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the bookmarks feature - empty means all")
fs.StringSlice("pdfengines-read-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the read bookmarks feature - empty means all")
fs.StringSlice("pdfengines-write-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the write bookmarks feature - empty means all")
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
// Deprecated flags.
@@ -85,8 +85,8 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
encryptNames := flags.MustStringSlice("pdfengines-encrypt-engines")
embedNames := flags.MustStringSlice("pdfengines-embed-engines")
bookmarksNames := flags.MustStringSlice("pdfengines-bookmarks-engines")
readBookmarksNames := flags.MustStringSlice("pdfengines-read-bookmarks-engines")
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-bookmarks-engines")
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
@@ -153,16 +153,16 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
mod.embedNames = embedNames
}
mod.writeBookmarksNames = defaultNames
if len(bookmarksNames) > 0 {
mod.writeBookmarksNames = bookmarksNames
}
mod.readBookmarksNames = defaultNames
if len(readBookmarksNames) > 0 {
mod.readBookmarksNames = readBookmarksNames
}
mod.writeBookmarksNames = defaultNames
if len(writeBookmarksNames) > 0 {
mod.writeBookmarksNames = writeBookmarksNames
}
return nil
}
@@ -212,8 +212,8 @@ func (mod *PdfEngines) Validate() error {
findNonExistingEngines(mod.writeMetadataNames)
findNonExistingEngines(mod.encryptNames)
findNonExistingEngines(mod.embedNames)
findNonExistingEngines(mod.writeBookmarksNames)
findNonExistingEngines(mod.readBookmarksNames)
findNonExistingEngines(mod.writeBookmarksNames)
if len(nonExistingEngines) == 0 {
return nil
@@ -234,8 +234,8 @@ func (mod *PdfEngines) SystemMessages() []string {
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
fmt.Sprintf("encrypt engines - %s", strings.Join(mod.encryptNames[:], " ")),
fmt.Sprintf("embed engines - %s", strings.Join(mod.embedNames[:], " ")),
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames[:], " ")),
fmt.Sprintf("read bookmarks engines - %s", strings.Join(mod.readBookmarksNames[:], " ")),
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames[:], " ")),
}
}
@@ -264,8 +264,8 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
engines(mod.writeMetadataNames),
engines(mod.encryptNames),
engines(mod.embedNames),
engines(mod.writeBookmarksNames),
engines(mod.readBookmarksNames),
engines(mod.writeBookmarksNames),
), nil
}

View File

@@ -322,6 +322,7 @@ func WriteBookmarksStub(ctx *api.Context, engine gotenberg.PdfEngine, bookmarks
}
}
default:
// Should not happen.
return fmt.Errorf("bookmarks type '%T' not supported", bookmarks)
}