mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 12:22:17 +01:00
fix(pdfengines): correctly update the indexes if the bookmarks form field (map format) is given
This commit is contained in:
@@ -51,6 +51,7 @@ type PdfEngineMock struct {
|
|||||||
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||||
|
PageCountMock func(ctx context.Context, logger *zap.Logger, inputPath string) (int, error)
|
||||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error
|
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error
|
||||||
ReadBookmarksMock func(ctx context.Context, logger *zap.Logger, inputPath string) ([]Bookmark, error)
|
ReadBookmarksMock func(ctx context.Context, logger *zap.Logger, inputPath string) ([]Bookmark, error)
|
||||||
EncryptMock func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error
|
EncryptMock func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error
|
||||||
@@ -78,6 +79,10 @@ func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logge
|
|||||||
return engine.ReadMetadataMock(ctx, logger, inputPath)
|
return engine.ReadMetadataMock(ctx, logger, inputPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||||
|
return engine.PageCountMock(ctx, logger, inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||||
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
|
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ type PdfEngine interface {
|
|||||||
// ReadMetadata extracts the metadata of a given PDF file.
|
// ReadMetadata extracts the metadata of a given PDF file.
|
||||||
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||||
|
|
||||||
|
// PageCount returns the number of pages in a PDF file.
|
||||||
|
PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error)
|
||||||
|
|
||||||
// WriteMetadata writes the metadata into a given PDF file.
|
// WriteMetadata writes the metadata into a given PDF file.
|
||||||
WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error
|
WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error
|
||||||
|
|
||||||
|
|||||||
@@ -203,6 +203,37 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
|||||||
return nil
|
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.
|
// WriteBookmarks is not available in this implementation.
|
||||||
func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
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)
|
return fmt.Errorf("write PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *z
|
|||||||
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
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.
|
// WriteBookmarks is not available in this implementation.
|
||||||
func (engine *LibreOfficePdfEngine) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
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)
|
return fmt.Errorf("write PDF bookmarks with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -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)
|
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.
|
// 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) {
|
func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||||
tmpPath := fmt.Sprintf("%s.read.json", inputPath)
|
tmpPath := fmt.Sprintf("%s.read.json", inputPath)
|
||||||
|
|||||||
@@ -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)
|
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 {
|
type readBookmarksResult struct {
|
||||||
bookmarks []gotenberg.Bookmark
|
bookmarks []gotenberg.Bookmark
|
||||||
err error
|
err error
|
||||||
|
|||||||
@@ -292,6 +292,21 @@ func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata ma
|
|||||||
return nil
|
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
|
// WriteBookmarksStub writes the bookmarks into PDF files. If no bookmarks, it
|
||||||
// does nothing.
|
// does nothing.
|
||||||
func WriteBookmarksStub(ctx *api.Context, engine gotenberg.PdfEngine, bookmarks any, inputPaths []string) error {
|
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)
|
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")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -425,8 +433,27 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var finalBookmarks []gotenberg.Bookmark
|
||||||
if b, ok := bookmarks.([]gotenberg.Bookmark); ok {
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("write bookmarks: %w", err)
|
return fmt.Errorf("write bookmarks: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
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.
|
// WriteBookmarks is not available in this implementation.
|
||||||
func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
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)
|
return fmt.Errorf("write PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -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)
|
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.
|
// WriteBookmarks is not available in this implementation.
|
||||||
func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
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)
|
return fmt.Errorf("write PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -104,8 +104,10 @@ Feature: /debug
|
|||||||
"pdfengines-engines": "[]",
|
"pdfengines-engines": "[]",
|
||||||
"pdfengines-flatten-engines": "[qpdf]",
|
"pdfengines-flatten-engines": "[qpdf]",
|
||||||
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
||||||
|
"pdfengines-read-bookmarks-engines": "[pdfcpu]",
|
||||||
"pdfengines-read-metadata-engines": "[exiftool]",
|
"pdfengines-read-metadata-engines": "[exiftool]",
|
||||||
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
||||||
|
"pdfengines-write-bookmarks-engines": "[pdfcpu]",
|
||||||
"pdfengines-write-metadata-engines": "[exiftool]",
|
"pdfengines-write-metadata-engines": "[exiftool]",
|
||||||
"prometheus-collect-interval": "1s",
|
"prometheus-collect-interval": "1s",
|
||||||
"prometheus-disable-collect": "false",
|
"prometheus-disable-collect": "false",
|
||||||
@@ -224,8 +226,10 @@ Feature: /debug
|
|||||||
"pdfengines-engines": "[]",
|
"pdfengines-engines": "[]",
|
||||||
"pdfengines-flatten-engines": "[qpdf]",
|
"pdfengines-flatten-engines": "[qpdf]",
|
||||||
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
||||||
|
"pdfengines-read-bookmarks-engines": "[pdfcpu]",
|
||||||
"pdfengines-read-metadata-engines": "[exiftool]",
|
"pdfengines-read-metadata-engines": "[exiftool]",
|
||||||
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
||||||
|
"pdfengines-write-bookmarks-engines": "[pdfcpu]",
|
||||||
"pdfengines-write-metadata-engines": "[exiftool]",
|
"pdfengines-write-metadata-engines": "[exiftool]",
|
||||||
"prometheus-collect-interval": "1s",
|
"prometheus-collect-interval": "1s",
|
||||||
"prometheus-disable-collect": "false",
|
"prometheus-disable-collect": "false",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@pdfengines
|
@pdfengines
|
||||||
@pdfengines-encrypt
|
@pdfengines-merge
|
||||||
@merge
|
@merge
|
||||||
Feature: /forms/pdfengines/merge
|
Feature: /forms/pdfengines/merge
|
||||||
|
|
||||||
@@ -130,6 +130,16 @@ Feature: /forms/pdfengines/merge
|
|||||||
"""
|
"""
|
||||||
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
|
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
|
||||||
"""
|
"""
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| files | testdata/page_2.pdf | file |
|
||||||
|
| bookmarks | foo | field |
|
||||||
|
Then the response status code should be 400
|
||||||
|
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||||
|
Then the response body should match string:
|
||||||
|
"""
|
||||||
|
Invalid form data: form field 'bookmarks' is invalid (got 'foo', resulting to unmarshal bookmarks: invalid character 'o' in literal false (expecting 'a'))
|
||||||
|
"""
|
||||||
|
|
||||||
@convert
|
@convert
|
||||||
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1)
|
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1)
|
||||||
@@ -203,6 +213,74 @@ Feature: /forms/pdfengines/merge
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@bookmarks
|
||||||
|
Scenario: POST /forms/pdfengines/merge (Bookmarks List)
|
||||||
|
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/page_1.pdf | file |
|
||||||
|
| files | testdata/page_2.pdf | file |
|
||||||
|
| bookmarks | [{"title":"Merged Index","page":1}] | 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": "Merged Index",
|
||||||
|
"page": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@bookmarks
|
||||||
|
Scenario: POST /forms/pdfengines/merge (Bookmarks Map)
|
||||||
|
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/page_1.pdf | file |
|
||||||
|
| files | testdata/page_2.pdf | file |
|
||||||
|
| bookmarks | {"page_1.pdf":[{"title":"Page 1 Index","page":1,"children":[{"title":"Page 1 Sub-index","page":1}]}],"page_2.pdf":[{"title":"Page 2 Index","page":1,"children":[{"title":"Page 2 Sub-index","page":1}]}]} | 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": "Page 1 Index",
|
||||||
|
"page": 1,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Page 1 Sub-index",
|
||||||
|
"page": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Page 2 Index",
|
||||||
|
"page": 2,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"title": "Page 2 Sub-index",
|
||||||
|
"page": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
@flatten
|
@flatten
|
||||||
Scenario: POST /forms/pdfengines/merge (Flatten)
|
Scenario: POST /forms/pdfengines/merge (Flatten)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
@@ -270,7 +348,8 @@ Feature: /forms/pdfengines/merge
|
|||||||
@metadata
|
@metadata
|
||||||
@flatten
|
@flatten
|
||||||
@embed
|
@embed
|
||||||
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
@bookmarks
|
||||||
|
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds & Bookmarks)
|
||||||
Given I have a default Gotenberg container
|
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):
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
||||||
| files | testdata/page_1.pdf | file |
|
| files | testdata/page_1.pdf | file |
|
||||||
@@ -278,6 +357,7 @@ Feature: /forms/pdfengines/merge
|
|||||||
| pdfa | PDF/A-1b | field |
|
| pdfa | PDF/A-1b | field |
|
||||||
| pdfua | true | field |
|
| pdfua | true | field |
|
||||||
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
|
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
|
||||||
|
| bookmarks | [{"title":"Merged Index","page":1}] | field |
|
||||||
| flatten | true | field |
|
| flatten | true | field |
|
||||||
| embeds | testdata/embed_1.xml | file |
|
| embeds | testdata/embed_1.xml | file |
|
||||||
| embeds | testdata/embed_2.xml | file |
|
| embeds | testdata/embed_2.xml | file |
|
||||||
@@ -301,6 +381,21 @@ Feature: /forms/pdfengines/merge
|
|||||||
Then the response PDF(s) should be flatten
|
Then the response PDF(s) should be flatten
|
||||||
Then the response PDF(s) should have the "embed_1.xml" file embedded
|
Then the response PDF(s) should have the "embed_1.xml" file embedded
|
||||||
Then the response PDF(s) should have the "embed_2.xml" file embedded
|
Then the response PDF(s) should have the "embed_2.xml" file embedded
|
||||||
|
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": "Merged Index",
|
||||||
|
"page": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
|
||||||
| files | teststore/foo.pdf | file |
|
| files | teststore/foo.pdf | file |
|
||||||
Then the response status code should be 200
|
Then the response status code should be 200
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type scenario struct {
|
|||||||
resp *httptest.ResponseRecorder
|
resp *httptest.ResponseRecorder
|
||||||
concurrentResps []*httptest.ResponseRecorder
|
concurrentResps []*httptest.ResponseRecorder
|
||||||
workdir string
|
workdir string
|
||||||
|
teststoreDir string
|
||||||
gotenbergContainer testcontainers.Container
|
gotenbergContainer testcontainers.Container
|
||||||
gotenbergContainerNetwork *testcontainers.DockerNetwork
|
gotenbergContainerNetwork *testcontainers.DockerNetwork
|
||||||
server *server
|
server *server
|
||||||
@@ -163,12 +164,14 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
|||||||
fields[name] = value
|
fields[name] = value
|
||||||
case "file":
|
case "file":
|
||||||
if strings.Contains(value, "teststore") {
|
if strings.Contains(value, "teststore") {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
if s.teststoreDir == "" {
|
||||||
_, err := os.Stat(dirPath)
|
return errors.New("no teststore directory available from previous requests")
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
|
||||||
}
|
}
|
||||||
value = strings.ReplaceAll(value, "teststore", dirPath)
|
_, err := os.Stat(s.teststoreDir)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("directory %q does not exist", s.teststoreDir)
|
||||||
|
}
|
||||||
|
value = strings.ReplaceAll(value, "teststore", s.teststoreDir)
|
||||||
} else {
|
} else {
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,6 +219,13 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
|||||||
return fmt.Errorf("write response body: %w", err)
|
return fmt.Errorf("write response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusNoContent {
|
||||||
|
// Gotenberg processes this asynchronously. The webhook test server
|
||||||
|
// will save the incoming files under this trace ID directory shortly.
|
||||||
|
s.teststoreDir = fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -241,6 +251,8 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
|||||||
return fmt.Errorf("create working directory: %w", err)
|
return fmt.Errorf("create working directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.teststoreDir = dirPath
|
||||||
|
|
||||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||||
file, err := os.Create(fpath)
|
file, err := os.Create(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -637,7 +649,7 @@ func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *godog.DocStr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
|
func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
dirPath := s.teststoreDir
|
||||||
|
|
||||||
_, err := os.Stat(dirPath)
|
_, err := os.Stat(dirPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -666,7 +678,7 @@ func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTable *godog.Table) error {
|
func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTable *godog.Table) error {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
dirPath := s.teststoreDir
|
||||||
|
|
||||||
_, err := os.Stat(dirPath)
|
_, err := os.Stat(dirPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -708,7 +720,7 @@ func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTable *godog
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Context, kind, validate string, tolerance int) error {
|
func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Context, kind, validate string, tolerance int) error {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
dirPath := s.teststoreDir
|
||||||
|
|
||||||
_, err := os.Stat(dirPath)
|
_, err := os.Stat(dirPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -788,7 +800,7 @@ func (s *scenario) thePdfShouldHavePages(ctx context.Context, name string, pages
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
substr := strings.ReplaceAll(name, "*_", "")
|
substr := strings.ReplaceAll(name, "*_", "")
|
||||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||||
if pathErr != nil {
|
if pathErr != nil {
|
||||||
return pathErr
|
return pathErr
|
||||||
}
|
}
|
||||||
@@ -844,7 +856,7 @@ func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
substr := strings.ReplaceAll(name, "*_", "")
|
substr := strings.ReplaceAll(name, "*_", "")
|
||||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||||
if pathErr != nil {
|
if pathErr != nil {
|
||||||
return pathErr
|
return pathErr
|
||||||
}
|
}
|
||||||
@@ -911,7 +923,7 @@ func (s *scenario) thePdfShouldHaveTheFollowingContentAtPage(ctx context.Context
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
substr := strings.ReplaceAll(name, "*_", "")
|
substr := strings.ReplaceAll(name, "*_", "")
|
||||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||||
if pathErr != nil {
|
if pathErr != nil {
|
||||||
return pathErr
|
return pathErr
|
||||||
}
|
}
|
||||||
@@ -955,7 +967,7 @@ func (s *scenario) thePdfShouldHaveTheFollowingContentAtPage(ctx context.Context
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should string) error {
|
func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should string) error {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
dirPath := s.teststoreDir
|
||||||
|
|
||||||
_, err := os.Stat(dirPath)
|
_, err := os.Stat(dirPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -1005,7 +1017,7 @@ func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind string, should string) error {
|
func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind string, should string) error {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
dirPath := s.teststoreDir
|
||||||
|
|
||||||
_, err := os.Stat(dirPath)
|
_, err := os.Stat(dirPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@@ -1057,7 +1069,7 @@ func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind string, sh
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *scenario) thePdfsShouldHaveEmbeddedFile(ctx context.Context, kind, should, embed string) error {
|
func (s *scenario) thePdfsShouldHaveEmbeddedFile(ctx context.Context, kind, should, embed string) error {
|
||||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
dirPath := s.teststoreDir
|
||||||
|
|
||||||
_, err := os.Stat(dirPath)
|
_, err := os.Stat(dirPath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user