chore(pdfengines): add stubs for common workflows

This commit is contained in:
Julien Neuhart
2024-11-25 09:28:42 +01:00
parent 1b49b35365
commit 42ee593708
5 changed files with 439 additions and 324 deletions

View File

@@ -21,6 +21,7 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api" "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
"github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
) )
// FormDataChromiumOptions creates [Options] from the form data. Fallback to // FormDataChromiumOptions creates [Options] from the form data. Fallback to
@@ -316,39 +317,6 @@ func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, Screens
return form, screenshotOptions return form, screenshotOptions
} }
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
// data. Fallback to default value if the considered key is not present.
func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
var (
pdfa string
pdfua bool
)
form.
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false)
return gotenberg.PdfFormats{
PdfA: pdfa,
PdfUa: pdfua,
}
}
// FormDataPdfMetadata creates metadata object from the form data.
func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
var metadata map[string]interface{}
form.Custom("metadata", func(value string) error {
if len(value) > 0 {
err := json.Unmarshal([]byte(value), &metadata)
if err != nil {
return fmt.Errorf("unmarshal metadata: %w", err)
}
}
return nil
})
return metadata
}
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF. // convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route { func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
@@ -358,8 +326,8 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := FormDataPdfMetadata(form) metadata := pdfengines.FormDataPdfMetadata(form)
var url string var url string
err := form. err := form.
@@ -418,8 +386,8 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := FormDataPdfMetadata(form) metadata := pdfengines.FormDataPdfMetadata(form)
var inputPath string var inputPath string
err := form. err := form.
@@ -480,8 +448,8 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(form) pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := FormDataPdfMetadata(form) metadata := pdfengines.FormDataPdfMetadata(form)
var ( var (
inputPath string inputPath string
@@ -664,34 +632,19 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
return fmt.Errorf("convert to PDF: %w", err) return fmt.Errorf("convert to PDF: %w", err)
} }
// So far so good, the URL has been converted to PDF. outputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, []string{outputPath})
// Now, let's check if the client want to convert the resulting PDF
// to specific formats.
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil { if err != nil {
return fmt.Errorf("convert PDF: %w", err) return fmt.Errorf("convert PDF: %w", err)
} }
// Important: the output path is now the converted file. err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths)
outputPath = convertOutputPath
}
// Writes and potentially overrides metadata entries, if any.
if len(metadata) > 0 {
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
} }
}
err = ctx.AddOutputPaths(outputPath) err = ctx.AddOutputPaths(outputPaths...)
if err != nil { if err != nil {
return fmt.Errorf("add output path: %w", err) return fmt.Errorf("add output paths: %w", err)
} }
return nil return nil

View File

@@ -603,125 +603,6 @@ func TestFormDataChromiumScreenshotOptions(t *testing.T) {
} }
} }
func TestFormDataChromiumPdfFormats(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
expectedPdfFormats gotenberg.PdfFormats
expectValidationError bool
}{
{
scenario: "no custom form fields",
ctx: &api.ContextMock{Context: new(api.Context)},
expectedPdfFormats: gotenberg.PdfFormats{},
expectValidationError: false,
},
{
scenario: "pdfa and pdfua form fields",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"pdfa": {
"foo",
},
"pdfua": {
"true",
},
})
return ctx
}(),
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
form := tc.ctx.Context.FormData()
actual := FormDataChromiumPdfFormats(form)
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
}
err := form.Validate()
if tc.expectValidationError && err == nil {
t.Fatal("expected validation error but got none", err)
}
if !tc.expectValidationError && err != nil {
t.Fatalf("expected no validation error but got: %v", err)
}
})
}
}
func TestFormDataPdfMetadata(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
expectedMetadata map[string]interface{}
expectValidationError bool
}{
{
scenario: "no metadata form field",
ctx: &api.ContextMock{Context: new(api.Context)},
expectedMetadata: nil,
expectValidationError: false,
},
{
scenario: "invalid metadata form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"metadata": {
"foo",
},
})
return ctx
}(),
expectedMetadata: nil,
expectValidationError: true,
},
{
scenario: "valid metadata form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"metadata": {
"{\"foo\":\"bar\"}",
},
})
return ctx
}(),
expectedMetadata: map[string]interface{}{
"foo": "bar",
},
expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
form := tc.ctx.Context.FormData()
actual := FormDataPdfMetadata(form)
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
}
err := form.Validate()
if tc.expectValidationError && err == nil {
t.Fatal("expected validation error but got none", err)
}
if !tc.expectValidationError && err != nil {
t.Fatalf("expected no validation error but got: %v", err)
}
})
}
}
func TestConvertUrlRoute(t *testing.T) { func TestConvertUrlRoute(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string

View File

@@ -13,6 +13,7 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
"github.com/gotenberg/gotenberg/v8/pkg/modules/api" "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api" libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
"github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
) )
// convertRoute returns an [api.Route] which can convert LibreOffice documents // convertRoute returns an [api.Route] which can convert LibreOffice documents
@@ -26,7 +27,10 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
defaultOptions := libreofficeapi.DefaultOptions() defaultOptions := libreofficeapi.DefaultOptions()
// Let's get the data from the form and validate them. form := ctx.FormData()
pdfFormats := pdfengines.FormDataPdfFormats(form)
metadata := pdfengines.FormDataPdfMetadata(form)
var ( var (
inputPaths []string inputPaths []string
password string password string
@@ -51,14 +55,11 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
quality int quality int
reduceImageResolution bool reduceImageResolution bool
maxImageResolution int maxImageResolution int
pdfa string
pdfua bool
nativePdfFormats bool nativePdfFormats bool
merge bool merge bool
metadata map[string]interface{}
) )
err := ctx.FormData(). err := form.
MandatoryPaths(libreOffice.Extensions(), &inputPaths). MandatoryPaths(libreOffice.Extensions(), &inputPaths).
String("password", &password, defaultOptions.Password). String("password", &password, defaultOptions.Password).
Bool("landscape", &landscape, defaultOptions.Landscape). Bool("landscape", &landscape, defaultOptions.Landscape).
@@ -120,8 +121,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
maxImageResolution = intValue maxImageResolution = intValue
return nil return nil
}). }).
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Bool("nativePdfFormats", &nativePdfFormats, true). Bool("nativePdfFormats", &nativePdfFormats, true).
Bool("merge", &merge, false). Bool("merge", &merge, false).
Custom("metadata", func(value string) error { Custom("metadata", func(value string) error {
@@ -138,12 +137,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
pdfFormats := gotenberg.PdfFormats{
PdfA: pdfa,
PdfUa: pdfua,
}
// Alright, let's convert each document to PDF.
outputPaths := make([]string, len(inputPaths)) outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") outputPaths[i] = ctx.GeneratePath(".pdf")
@@ -206,11 +199,8 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
// So far so good, let's check if we have to merge the PDFs. if merge {
if len(outputPaths) > 1 && merge { outputPath, err := pdfengines.MergeStub(ctx, engine, outputPaths)
outputPath := ctx.GeneratePath(".pdf")
err = engine.Merge(ctx, ctx.Log(), outputPaths, outputPath)
if err != nil { if err != nil {
return fmt.Errorf("merge PDFs: %w", err) return fmt.Errorf("merge PDFs: %w", err)
} }
@@ -219,35 +209,17 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
outputPaths = []string{outputPath} outputPaths = []string{outputPath}
} }
// Let's check if the client want to convert each PDF to a specific if !nativePdfFormats {
// PDF format. outputPaths, err = pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
zeroValued := gotenberg.PdfFormats{}
if !nativePdfFormats && pdfFormats != zeroValued {
convertOutputPaths := make([]string, len(outputPaths))
for i, outputPath := range outputPaths {
convertInputPath := outputPath
convertOutputPaths[i] = ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
if err != nil { if err != nil {
return fmt.Errorf("convert PDF: %w", err) return fmt.Errorf("convert PDFs: %w", err)
} }
} }
// Important: the output paths are now the converted files. err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths)
outputPaths = convertOutputPaths
}
// Writes and potentially overrides metadata entries, if any.
if len(metadata) > 0 {
for _, outputPath := range outputPaths {
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
} }
}
}
if len(outputPaths) > 1 { if len(outputPaths) > 1 {
// If .zip archive, document.docx -> document.docx.pdf. // If .zip archive, document.docx -> document.docx.pdf.
@@ -263,8 +235,6 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
// Last but not least, add the output paths to the context so that
// the API is able to send them as a response to the client.
err = ctx.AddOutputPaths(outputPaths...) err = ctx.AddOutputPaths(outputPaths...)
if err != nil { if err != nil {
return fmt.Errorf("add output paths: %w", err) return fmt.Errorf("add output paths: %w", err)

View File

@@ -13,6 +13,98 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/modules/api" "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
) )
// FormDataPdfFormats creates [gotenberg.PdfFormats] from the form data.
// Fallback to default value if the considered key is not present.
func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
var (
pdfa string
pdfua bool
)
form.
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false)
return gotenberg.PdfFormats{
PdfA: pdfa,
PdfUa: pdfua,
}
}
// FormDataPdfMetadata creates metadata object from the form data.
func FormDataPdfMetadata(form *api.FormData) map[string]interface{} {
var metadata map[string]interface{}
form.Custom("metadata", func(value string) error {
if len(value) > 0 {
err := json.Unmarshal([]byte(value), &metadata)
if err != nil {
return fmt.Errorf("unmarshal metadata: %w", err)
}
}
return nil
})
return metadata
}
// MergeStub merges given PDFs. If only one input PDF, it does nothing and
// returns the corresponding input path.
func MergeStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string) (string, error) {
if len(inputPaths) == 0 {
return "", errors.New("no input paths")
}
if len(inputPaths) == 1 {
return inputPaths[0], nil
}
outputPath := ctx.GeneratePath(".pdf")
err := engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
if err != nil {
return "", fmt.Errorf("merge %d PDFs: %w", len(inputPaths), err)
}
return outputPath, nil
}
// ConvertStub transforms a given PDF to the specified formats defined in
// [gotenberg.PdfFormats]. If no format, it does nothing and returns the input
// paths.
func ConvertStub(ctx *api.Context, engine gotenberg.PdfEngine, formats gotenberg.PdfFormats, inputPaths []string) ([]string, error) {
zeroValued := gotenberg.PdfFormats{}
if formats == zeroValued {
return inputPaths, nil
}
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf")
err := engine.Convert(ctx, ctx.Log(), formats, inputPath, outputPaths[i])
if err != nil {
return nil, fmt.Errorf("convert '%s': %w", inputPath, err)
}
}
return outputPaths, nil
}
// WriteMetadataStub writes the metadata into PDF files. If no metadata, it
// does nothing.
func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata map[string]interface{}, inputPaths []string) error {
if len(metadata) == 0 {
return nil
}
for _, inputPath := range inputPaths {
err := engine.WriteMetadata(ctx, ctx.Log(), metadata, inputPath)
if err != nil {
return fmt.Errorf("write metadata into '%s': %w", inputPath, err)
}
}
return nil
}
// mergeRoute returns an [api.Route] which can merge PDFs. // mergeRoute returns an [api.Route] which can merge PDFs.
func mergeRoute(engine gotenberg.PdfEngine) api.Route { func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
@@ -22,75 +114,37 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
// Let's get the data from the form and validate them. form := ctx.FormData()
var ( pdfFormats := FormDataPdfFormats(form)
inputPaths []string metadata := FormDataPdfMetadata(form)
pdfa string
pdfua bool
metadata map[string]interface{}
)
err := ctx.FormData(). var inputPaths []string
err := form.
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Custom("metadata", func(value string) error {
if len(value) > 0 {
err := json.Unmarshal([]byte(value), &metadata)
if err != nil {
return fmt.Errorf("unmarshal metadata: %w", err)
}
}
return nil
}).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
pdfFormats := gotenberg.PdfFormats{
PdfA: pdfa,
PdfUa: pdfua,
}
// Alright, let's merge the PDFs.
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 {
return fmt.Errorf("merge PDFs: %w", err) return fmt.Errorf("merge PDFs: %w", err)
} }
// So far so good, the PDFs are merged into one unique PDF. outputPaths, err := ConvertStub(ctx, engine, pdfFormats, []string{outputPath})
// Now, let's check if the client want to convert this result PDF
// to specific PDF formats.
zeroValued := gotenberg.PdfFormats{}
if pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil { if err != nil {
return fmt.Errorf("convert PDF: %w", err) return fmt.Errorf("convert PDF: %w", err)
} }
// Important: the output path is now the converted file. err = WriteMetadataStub(ctx, engine, metadata, outputPaths)
outputPath = convertOutputPath
}
// Writes and potentially overrides metadata entries, if any.
if len(metadata) > 0 {
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, outputPath)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
} }
}
// Last but not least, add the output path to the context so that err = ctx.AddOutputPaths(outputPaths...)
// the API is able to send it as a response to the client.
err = ctx.AddOutputPaths(outputPath)
if err != nil { if err != nil {
return fmt.Errorf("add output path: %w", err) return fmt.Errorf("add output paths: %w", err)
} }
return nil return nil
@@ -108,27 +162,17 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
// Let's get the data from the form and validate them. form := ctx.FormData()
var ( pdfFormats := FormDataPdfFormats(form)
inputPaths []string
pdfa string
pdfua bool
)
err := ctx.FormData(). var inputPaths []string
err := form.
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
pdfFormats := gotenberg.PdfFormats{
PdfA: pdfa,
PdfUa: pdfua,
}
zeroValued := gotenberg.PdfFormats{} zeroValued := gotenberg.PdfFormats{}
if pdfFormats == zeroValued { if pdfFormats == zeroValued {
return api.WrapError( return api.WrapError(
@@ -140,18 +184,14 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
) )
} }
// Alright, let's convert the PDFs. outputPaths, err := ConvertStub(ctx, engine, pdfFormats, inputPaths)
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
if err != nil { if err != nil {
return fmt.Errorf("convert PDF: %w", err) return fmt.Errorf("convert PDFs: %w", err)
} }
if len(outputPaths) > 1 { if len(outputPaths) > 1 {
// If .zip archive, keep the original filename. // If .zip archive, keep the original filename.
for i, inputPath := range inputPaths {
err = ctx.Rename(outputPaths[i], inputPath) err = ctx.Rename(outputPaths[i], inputPath)
if err != nil { if err != nil {
return fmt.Errorf("rename output path: %w", err) return fmt.Errorf("rename output path: %w", err)
@@ -161,8 +201,6 @@ func convertRoute(engine gotenberg.PdfEngine) api.Route {
} }
} }
// Last but not least, add the output paths to the context so that
// the API is able to send them as a response to the client.
err = ctx.AddOutputPaths(outputPaths...) err = ctx.AddOutputPaths(outputPaths...)
if err != nil { if err != nil {
return fmt.Errorf("add output paths: %w", err) return fmt.Errorf("add output paths: %w", err)
@@ -182,9 +220,7 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
// Let's get the data from the form and validate them.
var inputPaths []string var inputPaths []string
err := ctx.FormData(). err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
Validate() Validate()
@@ -192,7 +228,6 @@ func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
// Alright, let's read the metadata.
res := make(map[string]map[string]interface{}, len(inputPaths)) res := make(map[string]map[string]interface{}, len(inputPaths))
for _, inputPath := range inputPaths { for _, inputPath := range inputPaths {
metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath) metadata, err := engine.ReadMetadata(ctx, ctx.Log(), inputPath)
@@ -223,7 +258,6 @@ func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
// Let's get the data from the form and validate them.
var ( var (
inputPaths []string inputPaths []string
metadata map[string]interface{} metadata map[string]interface{}
@@ -248,16 +282,11 @@ func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
// Alright, let's convert the PDFs. err = WriteMetadataStub(ctx, engine, metadata, inputPaths)
for _, inputPath := range inputPaths {
err = engine.WriteMetadata(ctx, ctx.Log(), metadata, inputPath)
if err != nil { if err != nil {
return fmt.Errorf("write metadata: %w", err) return fmt.Errorf("write metadata: %w", err)
} }
}
// Last but not least, add the output paths to the context so that
// the API is able to send them as a response to the client.
err = ctx.AddOutputPaths(inputPaths...) err = ctx.AddOutputPaths(inputPaths...)
if err != nil { if err != nil {
return fmt.Errorf("add output paths: %w", err) return fmt.Errorf("add output paths: %w", err)

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect"
"slices" "slices"
"strings" "strings"
"testing" "testing"
@@ -16,6 +17,287 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/modules/api" "github.com/gotenberg/gotenberg/v8/pkg/modules/api"
) )
func TestFormDataPdfFormats(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
expectedPdfFormats gotenberg.PdfFormats
expectValidationError bool
}{
{
scenario: "no custom form fields",
ctx: &api.ContextMock{Context: new(api.Context)},
expectedPdfFormats: gotenberg.PdfFormats{},
expectValidationError: false,
},
{
scenario: "pdfa and pdfua form fields",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"pdfa": {
"foo",
},
"pdfua": {
"true",
},
})
return ctx
}(),
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
form := tc.ctx.Context.FormData()
actual := FormDataPdfFormats(form)
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
}
err := form.Validate()
if tc.expectValidationError && err == nil {
t.Fatal("expected validation error but got none", err)
}
if !tc.expectValidationError && err != nil {
t.Fatalf("expected no validation error but got: %v", err)
}
})
}
}
func TestFormDataPdfMetadata(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
expectedMetadata map[string]interface{}
expectValidationError bool
}{
{
scenario: "no metadata form field",
ctx: &api.ContextMock{Context: new(api.Context)},
expectedMetadata: nil,
expectValidationError: false,
},
{
scenario: "invalid metadata form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"metadata": {
"foo",
},
})
return ctx
}(),
expectedMetadata: nil,
expectValidationError: true,
},
{
scenario: "valid metadata form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"metadata": {
"{\"foo\":\"bar\"}",
},
})
return ctx
}(),
expectedMetadata: map[string]interface{}{
"foo": "bar",
},
expectValidationError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
form := tc.ctx.Context.FormData()
actual := FormDataPdfMetadata(form)
if !reflect.DeepEqual(actual, tc.expectedMetadata) {
t.Fatalf("expected %+v but got: %+v", tc.expectedMetadata, actual)
}
err := form.Validate()
if tc.expectValidationError && err == nil {
t.Fatal("expected validation error but got none", err)
}
if !tc.expectValidationError && err != nil {
t.Fatalf("expected no validation error but got: %v", err)
}
})
}
}
func TestMergeStub(t *testing.T) {
for _, tc := range []struct {
scenario string
engine gotenberg.PdfEngine
inputPaths []string
expectError bool
}{
{
scenario: "no input path (nil)",
inputPaths: nil,
expectError: true,
},
{
scenario: "no input path (empty)",
inputPaths: make([]string, 0),
expectError: true,
},
{
scenario: "only one input path",
inputPaths: []string{"my.pdf"},
expectError: false,
},
{
scenario: "merge error",
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
inputPaths: []string{"my.pdf", "my2.pdf"},
expectError: true,
},
{
scenario: "merge success",
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
inputPaths: []string{"my.pdf", "my2.pdf"},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
_, err := MergeStub(new(api.Context), tc.engine, tc.inputPaths)
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestConvertStub(t *testing.T) {
for _, tc := range []struct {
scenario string
engine gotenberg.PdfEngine
pdfFormats gotenberg.PdfFormats
expectError bool
}{
{
scenario: "no PDF formats",
pdfFormats: gotenberg.PdfFormats{},
expectError: false,
},
{
scenario: "convert error",
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
pdfFormats: gotenberg.PdfFormats{
PdfA: gotenberg.PdfA3b,
PdfUa: true,
},
expectError: true,
},
{
scenario: "convert success",
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
pdfFormats: gotenberg.PdfFormats{
PdfA: gotenberg.PdfA3b,
PdfUa: true,
},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
_, err := ConvertStub(new(api.Context), tc.engine, tc.pdfFormats, []string{"my.pdf", "my2.pdf"})
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestWriteMetadataStub(t *testing.T) {
for _, tc := range []struct {
scenario string
engine gotenberg.PdfEngine
metadata map[string]interface{}
expectError bool
}{
{
scenario: "no metadata (nil)",
metadata: nil,
expectError: false,
},
{
scenario: "no metadata (empty)",
metadata: make(map[string]interface{}, 0),
expectError: false,
},
{
scenario: "write metadata error",
engine: &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return errors.New("foo")
},
},
metadata: map[string]interface{}{"foo": "bar"},
expectError: true,
},
{
scenario: "write metadata success",
engine: &gotenberg.PdfEngineMock{
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
return nil
},
},
metadata: map[string]interface{}{"foo": "bar"},
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
err := WriteMetadataStub(new(api.Context), tc.engine, tc.metadata, []string{"my.pdf", "my2.pdf"})
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
})
}
}
func TestMergeHandler(t *testing.T) { func TestMergeHandler(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
scenario string scenario string