mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 19:32:15 +01:00
feat(pdfengines): generate document-title bookmarks when merging (#867)
This commit is contained in:
@@ -15,6 +15,7 @@ body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_2.pdf)
|
||||
~flatten: false
|
||||
~autoIndexBookmarks: false
|
||||
~titleBookmarks: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~optimizeImages: false
|
||||
|
||||
@@ -442,6 +442,27 @@ func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata ma
|
||||
return nil
|
||||
}
|
||||
|
||||
// documentTitle returns the input PDF's Title metadata entry, falling back to
|
||||
// the original filename without its extension when the entry is absent, blank,
|
||||
// or cannot be read. It labels the per-document entries the merge route's
|
||||
// titleBookmarks feature generates.
|
||||
func documentTitle(ctx *api.Context, engine gotenberg.PdfEngine, inputPath, filename string) string {
|
||||
fallback := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
|
||||
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
ctx.Log().WarnContext(ctx, fmt.Sprintf("read metadata of '%s' for title bookmark, using filename: %s", filename, err))
|
||||
return fallback
|
||||
}
|
||||
|
||||
title, ok := metadata["Title"].(string)
|
||||
if !ok || strings.TrimSpace(title) == "" {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
func shiftBookmarks(bookmarks []gotenberg.Bookmark, offset int) []gotenberg.Bookmark {
|
||||
if offset == 0 {
|
||||
return bookmarks
|
||||
@@ -995,10 +1016,12 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
var autoIndexBookmarks bool
|
||||
var titleBookmarks bool
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Bool("flatten", &flatten, false).
|
||||
Bool("autoIndexBookmarks", &autoIndexBookmarks, false).
|
||||
Bool("titleBookmarks", &titleBookmarks, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
@@ -1078,7 +1101,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
finalBookmarks = b
|
||||
} else {
|
||||
bMap, _ := bookmarks.(map[string][]gotenberg.Bookmark)
|
||||
if bMap != nil || autoIndexBookmarks {
|
||||
if bMap != nil || autoIndexBookmarks || titleBookmarks {
|
||||
offset := 0
|
||||
for _, inputPath := range inputPaths {
|
||||
filename := ctx.OriginalFilename(inputPath)
|
||||
@@ -1088,7 +1111,9 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
fileBookmarks = bMap[filename]
|
||||
}
|
||||
|
||||
if len(fileBookmarks) == 0 && autoIndexBookmarks {
|
||||
// titleBookmarks nests each input's own outline under
|
||||
// its title entry, so its outline is read here too.
|
||||
if len(fileBookmarks) == 0 && (autoIndexBookmarks || titleBookmarks) {
|
||||
fb, err := engine.ReadBookmarks(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bookmarks of '%s': %w", filename, err)
|
||||
@@ -1096,8 +1121,16 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
fileBookmarks = fb
|
||||
}
|
||||
|
||||
if len(fileBookmarks) > 0 {
|
||||
finalBookmarks = append(finalBookmarks, shiftBookmarks(fileBookmarks, offset)...)
|
||||
fileBookmarks = shiftBookmarks(fileBookmarks, offset)
|
||||
|
||||
if titleBookmarks {
|
||||
finalBookmarks = append(finalBookmarks, gotenberg.Bookmark{
|
||||
Title: documentTitle(ctx, engine, inputPath, filename),
|
||||
Page: offset + 1,
|
||||
Children: fileBookmarks,
|
||||
})
|
||||
} else if len(fileBookmarks) > 0 {
|
||||
finalBookmarks = append(finalBookmarks, fileBookmarks...)
|
||||
}
|
||||
|
||||
pageCount, err := engine.PageCount(ctx, ctx.Log(), inputPath)
|
||||
|
||||
@@ -291,6 +291,49 @@ Feature: /forms/pdfengines/merge
|
||||
}
|
||||
"""
|
||||
|
||||
# titleBookmarks adds a top-level bookmark per merged document, labeled by its
|
||||
# Title metadata (falling back to the filename) and pointing to its first page,
|
||||
# with the document's own outline nested underneath.
|
||||
# See https://github.com/gotenberg/gotenberg/issues/867.
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (Title Bookmarks)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
||||
| files | testdata/titled_alpha.pdf | file |
|
||||
| files | testdata/titled_bravo.pdf | file |
|
||||
| files | testdata/untitled_gamma.pdf | file |
|
||||
| titleBookmarks | true | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Alpha",
|
||||
"page": 1,
|
||||
"children": [
|
||||
{ "title": "A1", "page": 1 },
|
||||
{ "title": "A2", "page": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Bravo",
|
||||
"page": 3
|
||||
},
|
||||
{
|
||||
"title": "untitled_gamma",
|
||||
"page": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (Auto-index Bookmarks)
|
||||
Given I have a default Gotenberg container
|
||||
|
||||
BIN
test/integration/testdata/titled_alpha.pdf
vendored
Normal file
BIN
test/integration/testdata/titled_alpha.pdf
vendored
Normal file
Binary file not shown.
BIN
test/integration/testdata/titled_bravo.pdf
vendored
Normal file
BIN
test/integration/testdata/titled_bravo.pdf
vendored
Normal file
Binary file not shown.
BIN
test/integration/testdata/untitled_gamma.pdf
vendored
Normal file
BIN
test/integration/testdata/untitled_gamma.pdf
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user