feat(pdfengines): add PDF/UA (#714)

This commit is contained in:
Julien Neuhart
2023-11-05 20:14:36 +01:00
committed by GitHub
parent 9c3dfc78df
commit 143b7ce678
39 changed files with 1300 additions and 1013 deletions

View File

@@ -65,7 +65,7 @@ type Chromium struct {
logger *zap.Logger
browser browser
supervisor gotenberg.ProcessSupervisor
engine gotenberg.PDFEngine
engine gotenberg.PdfEngine
}
// Options are the available expectedOptions for converting HTML document to PDF.
@@ -293,11 +293,11 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"))
// PDF Engine.
provider, err := ctx.Module(new(gotenberg.PDFEngineProvider))
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
if err != nil {
return fmt.Errorf("get PDF engine provider: %w", err)
}
engine, err := provider.(gotenberg.PDFEngineProvider).PDFEngine()
engine, err := provider.(gotenberg.PdfEngineProvider).PdfEngine()
if err != nil {
return fmt.Errorf("get PDF engine: %w", err)
}
@@ -402,7 +402,7 @@ func (mod *Chromium) Checks() ([]health.CheckerOption, error) {
return nil
}
return errors.New("chromium is unhealthy")
return errors.New("Chromium is unhealthy")
},
}),
}, nil

View File

@@ -108,7 +108,7 @@ func TestChromium_Provision(t *testing.T) {
mod := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock
gotenberg.PdfEngineProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
@@ -116,7 +116,7 @@ func TestChromium_Provision(t *testing.T) {
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
return nil, errors.New("foo")
}
@@ -137,7 +137,7 @@ func TestChromium_Provision(t *testing.T) {
mod := &struct {
gotenberg.ModuleMock
gotenberg.LoggerProviderMock
gotenberg.PDFEngineProviderMock
gotenberg.PdfEngineProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
@@ -145,8 +145,8 @@ func TestChromium_Provision(t *testing.T) {
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
return zap.NewNop(), nil
}
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
return new(gotenberg.PDFEngineMock), nil
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
return new(gotenberg.PdfEngineMock), nil
}
return gotenberg.NewContext(

View File

@@ -129,8 +129,41 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
return form, options
}
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
// data. Fallback to default value if the considered key is not present.
func FormDataChromiumPdfFormats(ctx *api.Context) gotenberg.PdfFormats {
var (
pdfFormat string
pdfa string
pdfua bool
)
ctx.FormData().
String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false)
// FIXME: deprecated.
// pdfa > pdfFormat.
var actualPdfArchive string
if pdfFormat != "" {
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = pdfFormat
}
if pdfa != "" {
actualPdfArchive = pdfa
}
return gotenberg.PdfFormats{
PdfA: actualPdfArchive,
PdfUa: pdfua,
}
}
// 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{
Method: http.MethodPost,
Path: "/forms/chromium/convert/url",
@@ -138,21 +171,17 @@ func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(ctx)
var (
url string
pdfFormat string
)
var url string
err := form.
MandatoryString("url", &url).
String("pdfFormat", &pdfFormat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err)
}
@@ -164,7 +193,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
// PDF.
func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/chromium/convert/html",
@@ -172,23 +201,18 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(ctx)
var (
inputPath string
pdfFormat string
)
var inputPath string
err := form.
MandatoryPath("index.html", &inputPath).
String("pdfFormat", &pdfFormat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
url := fmt.Sprintf("file://%s", inputPath)
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err)
}
@@ -200,7 +224,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
// to PDF.
func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/chromium/convert/markdown",
@@ -208,17 +232,16 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(ctx)
var (
inputPath string
markdownPaths []string
pdfFormat string
)
err := form.
MandatoryPath("index.html", &inputPath).
MandatoryPaths([]string{".md"}, &markdownPaths).
String("pdfFormat", &pdfFormat, "").
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
@@ -297,7 +320,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
url := fmt.Sprintf("file://%s", inputPath)
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err)
}
@@ -308,7 +331,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
}
// convertUrl is a stub which is called by the other methods of this file.
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url, pdfFormat string, options Options) error {
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options Options) error {
outputPath := ctx.GeneratePath(".pdf")
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
@@ -385,22 +408,22 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url,
}
// So far so good, the URL has been converted to PDF.
// Now, let's check if the client want to convert this result PDF
// to a specific PDF format.
if pdfFormat != "" {
// 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(), pdfFormat, convertInputPath, convertOutputPath)
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
if err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError(
fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError(
http.StatusBadRequest,
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", pdfFormat),
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
),
)
}

View File

@@ -121,6 +121,58 @@ func TestFormDataChromiumPdfOptions(t *testing.T) {
}
}
func TestFormDataChromiumPdfFormats(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
expectedPdfFormats gotenberg.PdfFormats
}{
{
scenario: "no custom form fields",
ctx: &api.ContextMock{Context: new(api.Context)},
expectedPdfFormats: gotenberg.PdfFormats{},
},
{
scenario: "deprecated pdfFormat form field",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
},
{
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},
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
actual := FormDataChromiumPdfFormats(tc.ctx.Context)
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
}
})
}
}
func TestConvertUrlRoute(t *testing.T) {
for _, tc := range []struct {
scenario string
@@ -535,8 +587,8 @@ func TestConvertUrl(t *testing.T) {
scenario string
ctx *api.ContextMock
api Api
engine gotenberg.PDFEngine
pdfFormat string
engine gotenberg.PdfEngine
pdfFormats gotenberg.PdfFormats
options Options
expectError bool
expectHttpError bool
@@ -643,15 +695,15 @@ func TestConvertUrl(t *testing.T) {
expectOutputPathsCount: 0,
},
{
scenario: "ErrPDFFormatNotAvailable",
scenario: "ErrPdfFormatNotSupported",
ctx: &api.ContextMock{Context: new(api.Context)},
api: &ApiMock{func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return nil
}},
engine: &gotenberg.PDFEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported
}},
pdfFormat: "foo",
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
options: DefaultOptions(),
expectError: true,
expectHttpError: true,
@@ -664,10 +716,10 @@ func TestConvertUrl(t *testing.T) {
api: &ApiMock{func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return nil
}},
engine: &gotenberg.PDFEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
}},
pdfFormat: "foo",
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
options: DefaultOptions(),
expectError: true,
expectHttpError: false,
@@ -679,10 +731,10 @@ func TestConvertUrl(t *testing.T) {
api: &ApiMock{func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
return nil
}},
engine: &gotenberg.PDFEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
}},
pdfFormat: "foo",
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1a},
options: DefaultOptions(),
expectError: false,
expectHttpError: false,
@@ -717,7 +769,7 @@ func TestConvertUrl(t *testing.T) {
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.pdfFormat, tc.options)
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.pdfFormats, tc.options)
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)