mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-15 11:52:14 +01:00
feat(pdfengines): add PDF/UA (#714)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user