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

@@ -69,10 +69,10 @@ RUN \
# Credits:
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
curl -o ./ttf-mscorefonts-installer_3.8_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8_all.deb &&\
curl -o ./ttf-mscorefonts-installer_3.8.1_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8.1_all.deb &&\
apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
./ttf-mscorefonts-installer_3.8_all.deb \
./ttf-mscorefonts-installer_3.8.1_all.deb \
culmus \
fonts-beng \
fonts-hosny-amiri \
@@ -107,7 +107,7 @@ RUN \
fonts-noto-ui-core \
fonts-sil-gentium \
fonts-sil-gentium-basic &&\
rm -f ./ttf-mscorefonts-installer_3.8_all.deb &&\
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
# Add Color and Black-and-White Noto emoji font.
# Credits:
# https://github.com/gotenberg/gotenberg/pull/325.

12
go.mod
View File

@@ -5,7 +5,7 @@ go 1.21
require (
github.com/alexliesenfeld/health v0.8.0
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/chromedp/cdproto v0.0.0-20231025043423-5615e204d422
github.com/chromedp/cdproto v0.0.0-20231101223124-24f5925b5980
github.com/chromedp/chromedp v0.9.3
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.4.0
@@ -30,10 +30,10 @@ require (
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.17.0
golang.org/x/sync v0.4.0
golang.org/x/sys v0.13.0 // indirect
golang.org/x/sync v0.5.0
golang.org/x/sys v0.14.0 // indirect
golang.org/x/term v0.13.0
golang.org/x/text v0.13.0
golang.org/x/text v0.14.0
)
require (
@@ -44,8 +44,8 @@ require (
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gobwas/ws v1.3.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hhrutter/lzw v1.0.0 // indirect
github.com/hhrutter/tiff v1.0.1 // indirect
github.com/josharian/intern v1.0.0 // indirect

23
go.sum
View File

@@ -10,8 +10,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20231025043423-5615e204d422 h1:9d05eR3+VAcQqH5qiKbpMFa83XFK7jVDfAXIindFPbU=
github.com/chromedp/cdproto v0.0.0-20231025043423-5615e204d422/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20231101223124-24f5925b5980 h1:/nOO3ctHFqUmUwK5EdItjTCRQQCTcxWU7aJgyjFGtEc=
github.com/chromedp/cdproto v0.0.0-20231101223124-24f5925b5980/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
@@ -26,8 +26,9 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@@ -37,8 +38,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
@@ -147,19 +148,19 @@ golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

View File

@@ -24,27 +24,27 @@ func (mod *ValidatorMock) Validate() error {
return mod.ValidateMock()
}
// PDFEngineMock is a mock for the [PDFEngine] interface.
type PDFEngineMock struct {
// PdfEngineMock is a mock for the [PdfEngine] interface.
type PdfEngineMock struct {
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
}
func (engine *PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
}
func (engine *PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
}
// PDFEngineProviderMock is a mock for the [PDFEngineProvider] interface.
type PDFEngineProviderMock struct {
PDFEngineMock func() (PDFEngine, error)
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
type PdfEngineProviderMock struct {
PdfEngineMock func() (PdfEngine, error)
}
func (provider *PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
return provider.PDFEngineMock()
func (provider *PdfEngineProviderMock) PdfEngine() (PdfEngine, error) {
return provider.PdfEngineMock()
}
// ProcessMock is a mock for the [Process] interface.
@@ -113,8 +113,8 @@ func (provider *LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
var (
_ Module = (*ModuleMock)(nil)
_ Validator = (*ValidatorMock)(nil)
_ PDFEngine = (*PDFEngineMock)(nil)
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
_ PdfEngine = (*PdfEngineMock)(nil)
_ PdfEngineProvider = (*PdfEngineProviderMock)(nil)
_ Process = (*ProcessMock)(nil)
_ ProcessSupervisor = (*ProcessSupervisorMock)(nil)
_ LoggerProvider = (*LoggerProviderMock)(nil)

View File

@@ -35,36 +35,36 @@ func TestValidatorMock(t *testing.T) {
}
func TestPDFEngineMock(t *testing.T) {
mock := &PDFEngineMock{
mock := &PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
return nil
},
}
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
if err != nil {
t.Errorf("expected no error from PDFEngineMock.Merge, but got: %v", err)
t.Errorf("expected no error from PdfEngineMock.Merge, but got: %v", err)
}
err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
if err != nil {
t.Errorf("expected no error from PDFEngineMock.Convert, but got: %v", err)
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
}
}
func TestPDFEngineProviderMock(t *testing.T) {
mock := &PDFEngineProviderMock{
PDFEngineMock: func() (PDFEngine, error) {
return new(PDFEngineMock), nil
mock := &PdfEngineProviderMock{
PdfEngineMock: func() (PdfEngine, error) {
return new(PdfEngineMock), nil
},
}
_, err := mock.PDFEngine()
_, err := mock.PdfEngine()
if err != nil {
t.Errorf("expected no error from PDFEngineProviderMock.PDFEngine, but got: %v", err)
t.Errorf("expected no error from PdfEngineProviderMock.PdfEngine, but got: %v", err)
}
}

View File

@@ -8,45 +8,74 @@ import (
)
var (
// ErrPDFEngineMethodNotAvailable happens if a PDFEngine method is not
// available in the implementation.
ErrPDFEngineMethodNotAvailable = errors.New("method not available")
// ErrPdfEngineMethodNotSupported is returned when a specific method of the
// PdfEngine interface is not supported by its current implementation.
ErrPdfEngineMethodNotSupported = errors.New("method not supported")
// ErrPDFFormatNotAvailable happens if a PDFEngine Convert's method does
// not handle a specific format.
ErrPDFFormatNotAvailable = errors.New("PDF format not available")
// ErrPdfFormatNotSupported is returned when the Convert method of the
// PdfEngine interface does not support a requested PDF format conversion.
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
)
const (
FormatPDFA1a string = "PDF/A-1a"
FormatPDFA1b string = "PDF/A-1b"
FormatPDFA2a string = "PDF/A-2a"
FormatPDFA2b string = "PDF/A-2b"
FormatPDFA2u string = "PDF/A-2u"
FormatPDFA3a string = "PDF/A-3a"
FormatPDFA3b string = "PDF/A-3b"
FormatPDFA3u string = "PDF/A-3u"
// PdfA1a represents the PDF/A-1a format.
PdfA1a string = "PDF/A-1a"
// PdfA1b represents the PDF/A-1b format.
PdfA1b string = "PDF/A-1b"
// PdfA2a represents the PDF/A-2a format.
PdfA2a string = "PDF/A-2a"
// PdfA2b represents the PDF/A-2b format.
PdfA2b string = "PDF/A-2b"
// PdfA2u represents the PDF/A-2u format.
PdfA2u string = "PDF/A-2u"
// PdfA3a represents the PDF/A-3a format.
PdfA3a string = "PDF/A-3a"
// PdfA3b represents the PDF/A-3b format.
PdfA3b string = "PDF/A-3b"
// PdfA3u represents the PDF/A-3u format.
PdfA3u string = "PDF/A-3u"
)
// PDFEngine is a module interface which exposes methods for manipulating one
// or more PDFs. Implementations may abstract powerful tools like PDFtk, or
// fulfill those methods contracts in Golang directly.
type PDFEngine interface {
// Merge merges the given PDFs into a unique PDF. The pages' order reflects
// order of the given files.
// PdfFormats specifies the target formats for a PDF conversion.
type PdfFormats struct {
// PdfA denotes the PDF/A standard format (e.g., PDF/A-1a).
PdfA string
// PdfUa indicates whether the PDF should comply
// with the PDF/UA (Universal Accessibility) standard.
PdfUa bool
}
// PdfEngine provides an interface for operations on PDFs. Implementations
// can utilize various tools like PDFtk, or implement functionality directly in
// Go.
type PdfEngine interface {
// Merge combines multiple PDFs into a single PDF. The resulting page order
// is determined by the order of files provided in inputPaths.
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
// Convert converts the given PDF to a specific PDF format.
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
// Convert transforms a given PDF to the specified formats defined in
// PdfFormats. If no format, it does nothing.
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
}
// PDFEngineProvider is a module interface which exposes a method for creating a
// PDFEngine for other modules.
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
// This is used to decouple the creation of a [PdfEngine] from its consumers.
//
// Example:
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(gotenberg.PDFEngineProvider))
// pdfengines, _ := provider.(gotenberg.PDFEngineProvider).PDFEngine()
// provider, _ := ctx.Module(new(gotenberg.PdfEngineProvider))
// engine, _ := provider.(gotenberg.PdfEngineProvider).PdfEngine()
// }
type PDFEngineProvider interface {
PDFEngine() (PDFEngine, error)
type PdfEngineProvider interface {
// PdfEngine returns an instance of the PdfEngine interface for PDF operations.
PdfEngine() (PdfEngine, error)
}

View File

@@ -43,7 +43,7 @@ func (form FormData) Validate() error {
)
}
// String binds a form data value to a string variable.
// String binds a form field to a string variable.
//
// var foo string
//
@@ -52,17 +52,17 @@ func (form *FormData) String(key string, target *string, defaultValue string) *F
return form.mustValue(key, target, defaultValue)
}
// MandatoryString binds a form data value to a string variable. It populates
// MandatoryString binds a form field to a string variable. It populates
// an error if the value is empty or the "key" does not exist.
//
// var foo string
//
// ctx.FormData().MandatoryString("foo", &foo)
func (form *FormData) MandatoryString(key string, target *string) *FormData {
return form.mustMandatoryValue(key, target)
return form.mustMandatoryField(key, target)
}
// Bool binds a form data value to a bool variable. It populates an error if
// Bool binds a form field to a bool variable. It populates an error if
// the value is not bool.
//
// var foo bool
@@ -72,17 +72,17 @@ func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormDat
return form.mustValue(key, target, defaultValue)
}
// MandatoryBool binds a form data value to a bool variable. It populates an
// MandatoryBool binds a form field to a bool variable. It populates an
// error if the value is not bool, is empty, or the "key" does not exist.
//
// var foo bool
//
// ctx.FormData().MandatoryBool("foo", &foo)
func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
return form.mustMandatoryValue(key, target)
return form.mustMandatoryField(key, target)
}
// Int binds a form data value to an int variable. It populates an error if the
// Int binds a form field to an int variable. It populates an error if the
// value is not int.
//
// var foo int
@@ -92,17 +92,17 @@ func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
return form.mustValue(key, target, defaultValue)
}
// MandatoryInt binds a form data value to an int variable. It populates an
// MandatoryInt binds a form field to an int variable. It populates an
// error if the value is not int, is empty, or the "key" does not exist.
//
// var foo int
//
// ctx.FormData().MandatoryInt("foo", &foo)
func (form *FormData) MandatoryInt(key string, target *int) *FormData {
return form.mustMandatoryValue(key, target)
return form.mustMandatoryField(key, target)
}
// Float64 binds a form data value to a float64 variable. It populates an error
// Float64 binds a form field to a float64 variable. It populates an error
// if the value is not float64.
//
// var foo float64
@@ -112,18 +112,18 @@ func (form *FormData) Float64(key string, target *float64, defaultValue float64)
return form.mustValue(key, target, defaultValue)
}
// MandatoryFloat64 binds a form data value to a float64 variable. It populates
// MandatoryFloat64 binds a form field to a float64 variable. It populates
// an error if the is not float64, is empty, or the "key" does not exist.
//
// var foo float64
//
// ctx.FormData().MandatoryFloat64("foo", &foo)
func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
return form.mustMandatoryValue(key, target)
return form.mustMandatoryField(key, target)
}
// Duration binds a form data value to a time.Duration variable. It populates
// an error if the form data value is not time.Duration.
// Duration binds a form field to a time.Duration variable. It populates
// an error if the form field is not time.Duration.
//
// var foo time.Duration
//
@@ -132,7 +132,7 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
return form.mustValue(key, target, defaultValue)
}
// MandatoryDuration binds a form data value to a time.Duration variable. It
// MandatoryDuration binds a form field to a time.Duration variable. It
// populates an error if the value is not time.Duration, is empty, or the "key"
// does not exist.
//
@@ -140,10 +140,10 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
//
// ctx.FormData().MandatoryDuration("foo", &foo)
func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData {
return form.mustMandatoryValue(key, target)
return form.mustMandatoryField(key, target)
}
// Custom helps to define a custom binding function for a form data value.
// Custom helps to define a custom binding function for a form field.
//
// var foo map[string]string
//
@@ -168,16 +168,15 @@ func (form *FormData) Custom(key string, assign func(value string) error) *FormD
err := assign(value)
if err != nil {
form.append(
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
}
return form
}
// MandatoryCustom helps to define a custom binding function for a form data
// value. It populates an error if the value is empty or the "key" does not
// exist.
// MandatoryCustom helps to define a custom binding function for a form field.
// It populates an error if the value is empty or the "key" does not exist.
//
// var foo map[string]string
//
@@ -191,7 +190,7 @@ func (form *FormData) Custom(key string, assign func(value string) error) *FormD
// })
func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData {
var value string
form.mustMandatoryValue(key, &value)
form.mustMandatoryField(key, &value)
if value == "" {
return form
@@ -200,7 +199,7 @@ func (form *FormData) MandatoryCustom(key string, assign func(value string) erro
err := assign(value)
if err != nil {
form.append(
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
}
@@ -315,7 +314,7 @@ func (form *FormData) append(err error) {
form.errors = multierr.Append(form.errors, err)
}
// mustValue binds the target interface with a form data value. If the value is
// mustValue binds the target interface with a form field. If the value is
// empty or the "key" does not exist, it binds the default value. Currently,
// only the string, bool, int, float64 and time.Duration types are bindable.
func (form *FormData) mustValue(key string, target interface{}, defaultValue interface{}) *FormData {
@@ -343,16 +342,16 @@ func (form *FormData) mustValue(key string, target interface{}, defaultValue int
return form.mustAssign(key, val[0], target)
}
// mustMandatoryValue binds the target interface with a form data value. It
// mustMandatoryField binds the target interface with a form field. It
// populates an error if the value is empty or the "key" does not exist.
// Currently, only the string, bool, int, float64 and time.Duration types are
// bindable.
func (form *FormData) mustMandatoryValue(key string, target interface{}) *FormData {
func (form *FormData) mustMandatoryField(key string, target interface{}) *FormData {
val, ok := form.values[key]
if !ok || val[0] == "" {
form.append(
fmt.Errorf("form value '%s' is required", key),
fmt.Errorf("form field '%s' is required", key),
)
return form
@@ -386,7 +385,7 @@ func (form *FormData) mustAssign(key, value string, target interface{}) *FormDat
if err != nil {
form.append(
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
)
}

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)

View File

@@ -47,17 +47,17 @@ type Options struct {
Landscape bool
// PageRanges allows to select the pages to convert.
// TODO: should prefer a method form PDFEngine.
// TODO: should prefer a method form PdfEngine.
// Optional.
PageRanges string
// PdfFormat allows to convert the resulting PDF to PDF/A-1a, PDF/A-2b, or
// PDF/A-3b.
// PdfFormats allows to convert the resulting PDF to PDF/A-1a, PDF/A-2b,
// PDF/A-3b and PDF/UA.
// Optional.
PdfFormat string
PdfFormats gotenberg.PdfFormats
}
// Uno is an abstraction on top of Api.
// Uno is an abstraction on top of the Universal Network Objects API.
type Uno interface {
Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
Extensions() []string
@@ -265,13 +265,13 @@ func (a *Api) Metrics() ([]gotenberg.Metric, error) {
func (a *Api) Checks() ([]health.CheckerOption, error) {
return []health.CheckerOption{
health.WithCheck(health.Check{
Name: "api",
Name: "libreoffice",
Check: func(_ context.Context) error {
if a.supervisor.Healthy() {
return nil
}
return errors.New("LibreOffice unhealthy")
return errors.New("LibreOffice is unhealthy")
},
}),
}, nil

View File

@@ -270,18 +270,26 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
}
switch options.PdfFormat {
switch options.PdfFormats.PdfA {
case "":
case gotenberg.FormatPDFA1a:
case gotenberg.PdfA1a:
args = append(args, "--export", "SelectPdfVersion=1")
case gotenberg.FormatPDFA2b:
case gotenberg.PdfA2b:
args = append(args, "--export", "SelectPdfVersion=2")
case gotenberg.FormatPDFA3b:
case gotenberg.PdfA3b:
args = append(args, "--export", "SelectPdfVersion=3")
default:
return ErrInvalidPdfFormat
}
if options.PdfFormats.PdfUa {
args = append(
args,
"--export", "EnableTextAccessForAccessibilityTools=true",
"--export", "UseTaggedPDF=true",
)
}
args = append(args, "--output", outputPath, inputPath)
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)

View File

@@ -243,7 +243,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return p
}(),
fs: gotenberg.NewFileSystem(),
options: Options{PdfFormat: "foo"},
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}},
cancelledCtx: false,
start: false,
expectError: true,
@@ -417,7 +417,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return fs
}(),
options: Options{PdfFormat: gotenberg.FormatPDFA1a},
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1a}},
cancelledCtx: false,
start: true,
expectError: false,
@@ -446,7 +446,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return fs
}(),
options: Options{PdfFormat: gotenberg.FormatPDFA2b},
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA2b}},
cancelledCtx: false,
start: true,
expectError: false,
@@ -475,7 +475,36 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return fs
}(),
options: Options{PdfFormat: gotenberg.FormatPDFA3b},
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA3b}},
cancelledCtx: false,
start: true,
expectError: false,
},
{
scenario: "success (PDF/UA)",
libreOffice: newLibreOfficeProcess(
libreOfficeArguments{
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
unoBinPath: os.Getenv("UNOCONV_BIN_PATH"),
startTimeout: 5 * time.Second,
},
),
fs: func() *gotenberg.FileSystem {
fs := gotenberg.NewFileSystem()
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
if err != nil {
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
}
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
return fs
}(),
options: Options{PdfFormats: gotenberg.PdfFormats{PdfUa: true}},
cancelledCtx: false,
start: true,
expectError: false,

View File

@@ -18,7 +18,7 @@ func init() {
// PDF with LibreOffice.
type LibreOffice struct {
api libeofficeapi.Uno
engine gotenberg.PDFEngine
engine gotenberg.PdfEngine
disableRoutes bool
}
@@ -53,12 +53,12 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
mod.api = libreOfficeApi
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)
}

View File

@@ -94,7 +94,7 @@ func TestLibreOffice_Provision(t *testing.T) {
mod := &struct {
gotenberg.ModuleMock
libreofficeapi.ProviderMock
gotenberg.PDFEngineProviderMock
gotenberg.PdfEngineProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
@@ -102,7 +102,7 @@ func TestLibreOffice_Provision(t *testing.T) {
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
return new(libreofficeapi.ApiMock), nil
}
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
return nil, errors.New("foo")
}
@@ -123,7 +123,7 @@ func TestLibreOffice_Provision(t *testing.T) {
mod := &struct {
gotenberg.ModuleMock
libreofficeapi.ProviderMock
gotenberg.PDFEngineProviderMock
gotenberg.PdfEngineProviderMock
}{}
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
@@ -131,8 +131,8 @@ func TestLibreOffice_Provision(t *testing.T) {
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
return new(libreofficeapi.ApiMock), 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

@@ -1,4 +1,4 @@
// Package pdfengine provides a module which interacts with LibreOffice via the
// UNO (Universal Network Objects) API and implements the [gotenberg.PDFEngine]
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
// interface.
package pdfengine

View File

@@ -16,7 +16,7 @@ func init() {
}
// LibreOfficePdfEngine interacts with the LibreOffice (Universal Network Objects) API
// and implements the [gotenberg.PDFEngine] interface.
// and implements the [gotenberg.PdfEngine] interface.
type LibreOfficePdfEngine struct {
unoAPI api.Uno
}
@@ -46,17 +46,18 @@ func (engine *LibreOfficePdfEngine) Provision(ctx *gotenberg.Context) error {
return nil
}
// Merge is not available for this PDF engine.
func (engine *LibreOfficePdfEngine) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
// Merge is not available in this implementation.
func (engine *LibreOfficePdfEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Convert converts the given PDF to a specific PDF format. Currently, only the
// PDF/A-1a, PDF/A-2b and PDF/A-3b formats are available. If another PDF format
// is requested, it returns a [gotenberg.ErrPDFFormatNotAvailable] error.
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
// PDF/A-1a, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
// error.
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
err := engine.unoAPI.Pdf(ctx, logger, inputPath, outputPath, api.Options{
PdfFormat: format,
PdfFormats: formats,
})
if err == nil {
@@ -64,15 +65,15 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
}
if errors.Is(err, api.ErrInvalidPdfFormat) {
return fmt.Errorf("convert PDF to '%s' with LibreOffice: %w", format, gotenberg.ErrPDFFormatNotAvailable)
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, gotenberg.ErrPdfFormatNotSupported)
}
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, err)
}
// Interface guards.
var (
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
_ gotenberg.Provisioner = (*LibreOfficePdfEngine)(nil)
_ gotenberg.PDFEngine = (*LibreOfficePdfEngine)(nil)
_ gotenberg.PdfEngine = (*LibreOfficePdfEngine)(nil)
)

View File

@@ -95,8 +95,8 @@ func TestLibreOfficePdfEngine_Provider(t *testing.T) {
},
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(LibreOfficePdfEngine)
err := mod.Provision(tc.ctx)
engine := new(LibreOfficePdfEngine)
err := engine.Provision(tc.ctx)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
@@ -110,11 +110,11 @@ func TestLibreOfficePdfEngine_Provider(t *testing.T) {
}
func TestLibreOfficePdfEngine_Merge(t *testing.T) {
mod := new(LibreOfficePdfEngine)
err := mod.Merge(context.Background(), zap.NewNop(), nil, "")
engine := new(LibreOfficePdfEngine)
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
}
}
@@ -154,7 +154,7 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
} {
t.Run(tc.scenario, func(t *testing.T) {
engine := &LibreOfficePdfEngine{unoAPI: tc.api}
err := engine.Convert(context.Background(), zap.NewNop(), "", "", "")
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)

View File

@@ -14,7 +14,7 @@ import (
// convertRoute returns an [api.Route] which can convert LibreOffice documents
// to PDF.
func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) api.Route {
func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/libreoffice/convert",
@@ -30,6 +30,9 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
nativePdfA1aFormat bool
nativePdfFormat string
pdfFormat string
pdfa string
pdfua bool
nativePdfFormats bool
merge bool
)
@@ -40,50 +43,69 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
Bool("nativePdfA1aFormat", &nativePdfA1aFormat, false).
String("nativePdfFormat", &nativePdfFormat, "").
String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Bool("nativePdfFormats", &nativePdfFormats, true).
Bool("merge", &merge, false).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
// nativePdfFormat > pdfFormat > nativePdfA1aFormat.
// FIXME: deprecated.
// pdfa > nativePdfFormat > pdfFormat > nativePdfA1aFormat.
var (
actualPdfFormat string
nativeFormat bool
actualPdfArchive string
nativeFormats bool
)
// FIXME: deprecated.
if nativePdfA1aFormat {
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer 'nativePdfFormat' or 'pdfFormat' form fields instead")
actualPdfFormat = gotenberg.FormatPDFA1a
nativeFormat = true
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = gotenberg.PdfA1a
nativeFormats = true
}
if pdfFormat != "" {
actualPdfFormat = pdfFormat
nativeFormat = false
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = pdfFormat
nativeFormats = false
}
if nativePdfFormat != "" {
actualPdfFormat = nativePdfFormat
nativeFormat = true
ctx.Log().Warn("'nativePdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = nativePdfFormat
nativeFormats = true
}
if pdfa != "" {
actualPdfArchive = pdfa
nativeFormats = nativePdfFormats
}
if pdfua {
nativeFormats = nativePdfFormats
}
pdfFormats := gotenberg.PdfFormats{
PdfA: actualPdfArchive,
PdfUa: pdfua,
}
// Alright, let's convert each document to PDF.
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf")
options := libreofficeapi.Options{
Landscape: landscape,
PageRanges: nativePageRanges,
PdfFormat: nativePdfFormat,
}
if nativeFormats {
options.PdfFormats = pdfFormats
}
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
if err != nil {
if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) {
return api.WrapError(
@@ -108,21 +130,20 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
}
// Now, let's check if the client want to convert this result
// PDF to a specific PDF format.
if !nativeFormat && actualPdfFormat != "" {
// PDF to specific PDF formats.
zeroValued := gotenberg.PdfFormats{}
if !nativeFormats && pdfFormats != zeroValued {
convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), actualPdfFormat, 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", actualPdfFormat),
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),
),
)
}
@@ -147,23 +168,22 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
// Ok, we don't have to merge the PDFs. Let's check if the client
// want to convert each PDF to a specific PDF format.
if !nativeFormat && actualPdfFormat != "" {
zeroValued := gotenberg.PdfFormats{}
if !nativeFormats && 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(), actualPdfFormat, convertInputPath, convertOutputPaths[i])
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
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", actualPdfFormat),
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

@@ -19,7 +19,7 @@ func TestConvertRoute(t *testing.T) {
scenario string
ctx *api.ContextMock
libreOffice libreofficeapi.Uno
engine gotenberg.PDFEngine
engine gotenberg.PdfEngine
expectOptions libreofficeapi.Options
expectError bool
expectHttpError bool
@@ -81,7 +81,7 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 0,
},
{
scenario: "ErrPDFFormatNotAvailable (single file)",
scenario: "ErrPdfFormatNotSupported (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
@@ -102,9 +102,9 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
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
},
},
expectError: true,
@@ -121,7 +121,7 @@ func TestConvertRoute(t *testing.T) {
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
gotenberg.PdfA1a,
},
})
return ctx
@@ -134,8 +134,8 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
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")
},
},
@@ -209,15 +209,18 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 2,
},
{
scenario: "success with PDF format (single file)",
scenario: "success with non-native PDF/A (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"document.docx": "/document.docx",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
"nativePdfFormats": {
"false",
},
})
return ctx
@@ -230,8 +233,8 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
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
},
},
@@ -240,7 +243,7 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 1,
},
{
scenario: "success with every PDF formats form field (single file)",
scenario: "success with every non-native PDF/A & PDF/UA form fields (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
@@ -251,10 +254,19 @@ func TestConvertRoute(t *testing.T) {
"true",
},
"pdfFormat": {
gotenberg.FormatPDFA1a,
gotenberg.PdfA1a,
},
"nativePdfFormat": {
gotenberg.FormatPDFA1a,
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
"nativePdfFormats": {
"false",
},
})
return ctx
@@ -267,8 +279,8 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
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
},
},
@@ -299,7 +311,7 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
@@ -309,7 +321,7 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 0,
},
{
scenario: "ErrPDFFormatNotAvailable (merge)",
scenario: "ErrPdfFormatNotSupported (merge)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
@@ -320,9 +332,12 @@ func TestConvertRoute(t *testing.T) {
"merge": {
"true",
},
"pdfFormat": {
"pdfa": {
"foo",
},
"nativePdfFormats": {
"false",
},
})
return ctx
}(),
@@ -334,12 +349,12 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported
},
},
expectError: true,
@@ -359,8 +374,11 @@ func TestConvertRoute(t *testing.T) {
"merge": {
"true",
},
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
"nativePdfFormats": {
"false",
},
})
return ctx
@@ -373,11 +391,11 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
@@ -409,7 +427,7 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -441,7 +459,7 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -451,7 +469,7 @@ func TestConvertRoute(t *testing.T) {
expectOutputPathsCount: 1,
},
{
scenario: "success with PDF format (merge)",
scenario: "success with non-native PDF/A (merge)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
@@ -462,8 +480,11 @@ func TestConvertRoute(t *testing.T) {
"merge": {
"true",
},
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
"nativePdfFormats": {
"false",
},
})
return ctx
@@ -476,11 +497,11 @@ func TestConvertRoute(t *testing.T) {
return []string{".docx"}
},
},
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},

View File

@@ -1,3 +1,3 @@
// Package logging provides a module which creates a [zap.Logger] for other
// modules.
// Package logging provides a module which creates a zap.Logger instance for
// other modules.
package logging

View File

@@ -14,6 +14,7 @@ import (
func TestLogging_Descriptor(t *testing.T) {
descriptor := new(Logging).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(Logging))

View File

@@ -1,4 +1,6 @@
// Package pdfcpu provides a module which wraps the
// https://github.com/pdfcpu/pdfcpu library and implements the
// gotenberg.PDFEngine interface.
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
// interface using the pdfcpu library. This package allows for the merging of
// PDF files but does not support conversion to specific PDF formats.
//
// See: https://github.com/pdfcpu/pdfcpu.
package pdfcpu

View File

@@ -13,35 +13,34 @@ import (
)
func init() {
gotenberg.MustRegisterModule(PDFcpu{})
gotenberg.MustRegisterModule(new(PdfCpu))
}
// PDFcpu is a module which wraps the https://github.com/pdfcpu/pdfcpu library
// and implements the gotenberg.PDFEngine interface.
type PDFcpu struct {
// PdfCpu abstracts the pdfcpu library and implements the [gotenberg.PdfEngine]
// interface.
type PdfCpu struct {
conf *pdfcpuConfig.Configuration
}
// Descriptor returns a PDFcpu's module descriptor.
func (PDFcpu) Descriptor() gotenberg.ModuleDescriptor {
// Descriptor returns a [PdfCpu]'s module descriptor.
func (engine *PdfCpu) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "pdfcpu",
New: func() gotenberg.Module { return new(PDFcpu) },
New: func() gotenberg.Module { return new(PdfCpu) },
}
}
// Provision sets the engine properties.
func (engine *PDFcpu) Provision(_ *gotenberg.Context) error {
func (engine *PdfCpu) Provision(ctx *gotenberg.Context) error {
pdfcpuConfig.ConfigPath = "disable"
pdfcpuLog.DisableLoggers()
engine.conf = pdfcpuConfig.NewDefaultConfiguration()
return nil
}
// Merge merges the given PDFs into a unique Pdf.
func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string, outputPath string) error {
// Merge combines multiple PDFs into a single PDF.
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
if err == nil {
return nil
@@ -50,14 +49,14 @@ func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
}
// Convert is not available for this Pdf engine.
func (engine PDFcpu) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
return fmt.Errorf("convert Pdf to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
// Convert is not available in this implementation.
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
}
// Interface guards.
var (
_ gotenberg.Module = (*PDFcpu)(nil)
_ gotenberg.Provisioner = (*PDFcpu)(nil)
_ gotenberg.PDFEngine = (*PDFcpu)(nil)
_ gotenberg.Module = (*PdfCpu)(nil)
_ gotenberg.Provisioner = (*PdfCpu)(nil)
_ gotenberg.PdfEngine = (*PdfCpu)(nil)
)

View File

@@ -12,89 +12,93 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestPDFcpu_Descriptor(t *testing.T) {
descriptor := PDFcpu{}.Descriptor()
func TestPdfCpu_Descriptor(t *testing.T) {
descriptor := new(PdfCpu).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(PDFcpu))
expect := reflect.TypeOf(new(PdfCpu))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestPDFcpu_Provision(t *testing.T) {
mod := new(PDFcpu)
func TestPdfCpu_Provision(t *testing.T) {
engine := new(PdfCpu)
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
err := mod.Provision(ctx)
err := engine.Provision(ctx)
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
func TestPDFcpu_Merge(t *testing.T) {
for i, tc := range []struct {
func TestPdfCpu_Merge(t *testing.T) {
for _, tc := range []struct {
scenario string
inputPaths []string
expectErr bool
expectError bool
}{
{
scenario: "invalid input path",
inputPaths: []string{
"foo",
},
expectError: true,
},
{
scenario: "single file success",
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
},
expectError: false,
},
{
scenario: "many files success",
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
"/tests/test/testdata/pdfengines/sample2.pdf",
},
},
{
inputPaths: []string{
"foo",
},
expectErr: true,
},
} {
func() {
mod := new(PDFcpu)
err := mod.Provision(nil)
t.Run(tc.scenario, func(t *testing.T) {
engine := new(PdfCpu)
err := engine.Provision(nil)
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
t.Fatalf("expected error but got: %v", err)
}
fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
t.Fatalf("expected error but got: %v", err)
}
defer func() {
err := os.RemoveAll(fs.WorkingDirPath())
err = os.RemoveAll(fs.WorkingDirPath())
if err != nil {
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
t.Fatalf("expected no error while cleaning up but got: %v", err)
}
}()
err = mod.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
err = engine.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
}()
})
}
}
func TestPDFcpu_Convert(t *testing.T) {
mod := new(PDFcpu)
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
func TestPdfCpu_Convert(t *testing.T) {
mod := new(PdfCpu)
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
}
}

View File

@@ -1,3 +1,3 @@
// Package pdfengines provides a module which gathers modules that implements
// the gotenberg.PDFEngine interface.
// Package pdfengines a way to gather and manage multiple modules that
// implement the gotenberg.PdfEngine interface.
package pdfengines

View File

@@ -10,30 +10,24 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one
// or more gotenberg.PDFEngine. It provides a sort of fallback mechanism: if an
// engine's method returns an error, it calls the same method from another
// engine.
type multiPDFEngines struct {
engines []gotenberg.PDFEngine
type multiPdfEngines struct {
engines []gotenberg.PdfEngine
}
// newMultiPDFEngines returns a multiPDFEngines. Arguments' order determines the
// order of the engines called.
func newMultiPDFEngines(engines ...gotenberg.PDFEngine) *multiPDFEngines {
return &multiPDFEngines{
func newMultiPdfEngines(engines ...gotenberg.PdfEngine) *multiPdfEngines {
return &multiPdfEngines{
engines: engines,
}
}
// Merge tries to merge the given PDFs into a unique PDF thanks to its
// children. If the context is done, it stops and returns an error.
func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
var err error
errChan := make(chan error, 1)
for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
}(engine)
@@ -53,13 +47,13 @@ func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inpu
// Convert converts the given PDF to a specific PDF format. thanks to its
// children. If the context is done, it stops and returns an error.
func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
var err error
errChan := make(chan error, 1)
for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) {
errChan <- engine.Convert(ctx, logger, format, inputPath, outputPath)
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
}(engine)
select {
@@ -73,10 +67,10 @@ func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, fo
}
}
return fmt.Errorf("convert PDF to '%s' with multi PDF engines: %w", format, err)
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
}
// Interface guards.
var (
_ gotenberg.PDFEngine = (*multiPDFEngines)(nil)
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
)

View File

@@ -10,61 +10,63 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestMultiPDFEngines_Merge(t *testing.T) {
tests := []struct {
name string
engine *multiPDFEngines
func TestMultiPdfEngines_Merge(t *testing.T) {
for _, tc := range []struct {
scenario string
engine *multiPdfEngines
ctx context.Context
expectMergeErr bool
expectError bool
}{
{
name: "nominal behavior",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "nominal behavior",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
),
ctx: context.Background(),
expectError: false,
},
{
name: "at least one engine does not return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "at least one engine does not return an error",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
&gotenberg.PDFEngineMock{
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
),
ctx: context.Background(),
expectError: false,
},
{
name: "all engines return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "all engines return an error",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
&gotenberg.PDFEngineMock{
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
),
ctx: context.Background(),
expectMergeErr: true,
expectError: true,
},
{
name: "context expired",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
scenario: "context expired",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
@@ -76,37 +78,35 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
return ctx
}(),
expectMergeErr: true,
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
if tc.expectMergeErr && err == nil {
t.Errorf("expected engine.Merge() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectMergeErr && err != nil {
t.Errorf("expected no error from engine.Merge(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestMultiPDFEngines_Convert(t *testing.T) {
tests := []struct {
name string
engine *multiPDFEngines
func TestMultiPdfEngines_Convert(t *testing.T) {
for _, tc := range []struct {
scenario string
engine *multiPdfEngines
ctx context.Context
expectConvertErr bool
expectError bool
}{
{
name: "nominal behavior",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "nominal behavior",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
@@ -114,15 +114,15 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
ctx: context.Background(),
},
{
name: "at least one engine does not return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "at least one engine does not return an error",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
@@ -130,27 +130,27 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
ctx: context.Background(),
},
{
name: "all engines return an error",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "all engines return an error",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
),
ctx: context.Background(),
expectConvertErr: true,
expectError: true,
},
{
name: "context expired",
engine: newMultiPDFEngines(
&gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
scenario: "context expired",
engine: newMultiPdfEngines(
&gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
@@ -161,20 +161,18 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
return ctx
}(),
expectConvertErr: true,
expectError: true,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
err := tc.engine.Convert(tc.ctx, zap.NewNop(), gotenberg.PdfFormats{}, "", "")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.engine.Convert(tc.ctx, zap.NewNop(), "", "", "")
if tc.expectConvertErr && err == nil {
t.Errorf("expected engine.Convert() error, but got none")
}
if !tc.expectConvertErr && err != nil {
t.Errorf("expected no error from engine.Convert(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}

View File

@@ -12,28 +12,28 @@ import (
)
func init() {
gotenberg.MustRegisterModule(PDFEngines{})
gotenberg.MustRegisterModule(new(PdfEngines))
}
// PDFEngines is a module which gathers available gotenberg.PDFEngine modules.
// The available gotenberg.PDFEngine modules can be either all
// gotenberg.PDFEngine modules or the modules selected by the user thanks to
// the "engines" flag.
// PdfEngines acts as an aggregator and manager for multiple PDF engine
// modules. It enables the selection and ordering of PDF engines based on user
// preferences passed via command-line flags. The [PdfEngines] module also
// implements the [gotenberg.PdfEngine] interface, providing a unified approach
// to PDF processing across the various engines it manages.
//
// PDFEngines wraps the gotenberg.PDFEngine modules in an internal struct which
// also implements gotenberg.PDFEngine. This struct provides a sort of fallback
// mechanism: if an engine's method returns an error, it calls the same method
// from another engine.
//
// This module implements the gotenberg.PDFEngineProvider interface.
type PDFEngines struct {
// When processing PDFs, [PdfEngines] will attempt to use the engines in the
// order they were defined. If the primary engine encounters an error,
// [PdfEngines] can fall back to the next available engine. It also implements
// the [api.Router] interface to expose relevant PDF processing routes if
// enabled.
type PdfEngines struct {
names []string
engines []gotenberg.PDFEngine
engines []gotenberg.PdfEngine
disableRoutes bool
}
// Descriptor returns a PDFEngines' module descriptor.
func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
// Descriptor returns a PdfEngines' module descriptor.
func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "pdfengines",
FlagSet: func() *flag.FlagSet {
@@ -43,13 +43,13 @@ func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
return fs
}(),
New: func() gotenberg.Module { return new(PDFEngines) },
New: func() gotenberg.Module { return new(PdfEngines) },
}
}
// Provision gets either all gotenberg.PDFEngine modules or the modules
// Provision gets either all [gotenberg.PdfEngine] modules or the modules
// selected by the user thanks to the "engines" flag.
func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
flags := ctx.ParsedFlags()
names := flags.MustStringSlice("pdfengines-engines")
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
@@ -66,15 +66,15 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
logger = logger.Named("pdfengines")
engines, err := ctx.Modules(new(gotenberg.PDFEngine))
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
if err != nil {
return fmt.Errorf("get PDF engines: %w", err)
}
mod.engines = make([]gotenberg.PDFEngine, len(engines))
mod.engines = make([]gotenberg.PdfEngine, len(engines))
for i, engine := range engines {
mod.engines[i] = engine.(gotenberg.PDFEngine)
mod.engines[i] = engine.(gotenberg.PdfEngine)
}
if len(names) > 0 {
@@ -82,9 +82,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
mod.names = names
for i, name := range names {
logger.Warn("unoconv-pdfengine is deprecated; prefer api-pdfengine instead")
if name == "unoconv-pdfengine" {
mod.names[i] = "api-pdfengine"
// FIXME: deprecated.
if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
mod.names[i] = "libreoffice-pdfengine"
}
}
@@ -101,10 +102,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
return nil
}
// Validate validates there is at least one gotenberg.PDFEngine module
// available. It also validates that selected gotenberg.PDFEngine modules
// Validate validates there is at least one [gotenberg.PdfEngine] module
// available. It also validates that selected [gotenberg.PdfEngine] modules
// actually exist.
func (mod PDFEngines) Validate() error {
func (mod *PdfEngines) Validate() error {
if len(mod.engines) == 0 {
return errors.New("no PDF engine")
}
@@ -139,17 +140,17 @@ func (mod PDFEngines) Validate() error {
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
}
// SystemMessages returns one message with the selected gotenberg.PDFEngine
// SystemMessages returns one message with the selected [gotenberg.PdfEngine]
// modules.
func (mod PDFEngines) SystemMessages() []string {
func (mod *PdfEngines) SystemMessages() []string {
return []string{
strings.Join(mod.names[:], " "),
}
}
// PDFEngine returns a gotenberg.PDFEngine.
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
engines := make([]gotenberg.PDFEngine, len(mod.names))
// PdfEngine returns a [gotenberg.PdfEngine].
func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
engines := make([]gotenberg.PdfEngine, len(mod.names))
for i, name := range mod.names {
for _, engine := range mod.engines {
@@ -160,20 +161,20 @@ func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
}
}
return newMultiPDFEngines(engines...), nil
return newMultiPdfEngines(engines...), nil
}
// Routes returns the HTTP routes.
func (mod PDFEngines) Routes() ([]api.Route, error) {
func (mod *PdfEngines) Routes() ([]api.Route, error) {
if mod.disableRoutes {
return nil, nil
}
engine, err := mod.PDFEngine()
engine, err := mod.PdfEngine()
if err != nil {
// Should not happen, unless our provider implementation
// changes in the future.
return nil, fmt.Errorf("get pdf engine: %w", err)
return nil, fmt.Errorf("get pdf mod: %w", err)
}
return []api.Route{
@@ -184,10 +185,10 @@ func (mod PDFEngines) Routes() ([]api.Route, error) {
// Interface guards.
var (
_ gotenberg.Module = (*PDFEngines)(nil)
_ gotenberg.Provisioner = (*PDFEngines)(nil)
_ gotenberg.Validator = (*PDFEngines)(nil)
_ gotenberg.SystemLogger = (*PDFEngines)(nil)
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
_ api.Router = (*PDFEngines)(nil)
_ gotenberg.Module = (*PdfEngines)(nil)
_ gotenberg.Provisioner = (*PdfEngines)(nil)
_ gotenberg.Validator = (*PdfEngines)(nil)
_ gotenberg.SystemLogger = (*PdfEngines)(nil)
_ gotenberg.PdfEngineProvider = (*PdfEngines)(nil)
_ api.Router = (*PdfEngines)(nil)
)

View File

@@ -11,26 +11,26 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestPDFEngines_Descriptor(t *testing.T) {
descriptor := PDFEngines{}.Descriptor()
func TestPdfEngines_Descriptor(t *testing.T) {
descriptor := new(PdfEngines).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(PDFEngines))
expect := reflect.TypeOf(new(PdfEngines))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestPDFEngines_Provision(t *testing.T) {
tests := []struct {
name string
func TestPdfEngines_Provision(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx *gotenberg.Context
expectPDFEngineNames []string
expectProvisionErr bool
expectedPdfEngines []string
expectError bool
}{
{
name: "no selection from user",
scenario: "no selection from user",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -48,7 +48,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
@@ -59,7 +59,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
@@ -67,10 +67,11 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectPDFEngineNames: []string{"bar"},
expectedPdfEngines: []string{"bar"},
expectError: false,
},
{
name: "selection from user",
scenario: "selection from user",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -88,7 +89,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
@@ -100,7 +101,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
@@ -109,10 +110,10 @@ func TestPDFEngines_Provision(t *testing.T) {
return nil
}
fs := new(PDFEngines).Descriptor().FlagSet
fs := new(PdfEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
@@ -126,10 +127,11 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectPDFEngineNames: []string{"b", "a"},
expectedPdfEngines: []string{"b", "a"},
expectError: false,
},
{
name: "user select deprecated unoconv-pdfengine",
scenario: "user select deprecated unoconv-pdfengine",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -147,19 +149,19 @@ func TestPDFEngines_Provision(t *testing.T) {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "api-pdfengine", New: func() gotenberg.Module { return engine }}
return gotenberg.ModuleDescriptor{ID: "libreoffice-pdfengine", New: func() gotenberg.Module { return engine }}
}
engine.ValidateMock = func() error {
return nil
}
fs := new(PDFEngines).Descriptor().FlagSet
fs := new(PdfEngines).Descriptor().FlagSet
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
if err != nil {
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
t.Fatalf("expected no error but got: %v", err)
}
return gotenberg.NewContext(
@@ -172,22 +174,23 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectPDFEngineNames: []string{"api-pdfengine"},
expectedPdfEngines: []string{"libreoffice-pdfengine"},
expectError: false,
},
{
name: "no logger provider",
scenario: "no logger provider",
ctx: func() *gotenberg.Context {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{},
)
}(),
expectProvisionErr: true,
expectError: true,
},
{
name: "no logger from logger provider",
scenario: "no logger from logger provider",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -204,17 +207,17 @@ func TestPDFEngines_Provision(t *testing.T) {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
},
)
}(),
expectProvisionErr: true,
expectError: true,
},
{
name: "no valid PDF engines",
scenario: "no valid PDF engine",
ctx: func() *gotenberg.Context {
provider := &struct {
gotenberg.ModuleMock
@@ -232,7 +235,7 @@ func TestPDFEngines_Provision(t *testing.T) {
engine := &struct {
gotenberg.ModuleMock
gotenberg.ValidatorMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
@@ -243,7 +246,7 @@ func TestPDFEngines_Provision(t *testing.T) {
return gotenberg.NewContext(
gotenberg.ParsedFlags{
FlagSet: new(PDFEngines).Descriptor().FlagSet,
FlagSet: new(PdfEngines).Descriptor().FlagSet,
},
[]gotenberg.ModuleDescriptor{
provider.Descriptor(),
@@ -251,67 +254,66 @@ func TestPDFEngines_Provision(t *testing.T) {
},
)
}(),
expectProvisionErr: true,
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mod := new(PDFEngines)
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(PdfEngines)
err := mod.Provision(tc.ctx)
if tc.expectProvisionErr && err == nil {
t.Fatal("expected mod.Provision() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectProvisionErr && err != nil {
t.Fatalf("expected no error from mod.Provision(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
if len(tc.expectPDFEngineNames) != len(mod.names) {
t.Errorf("expected %d names but got %d", len(tc.expectPDFEngineNames), len(mod.names))
if len(tc.expectedPdfEngines) != len(mod.names) {
t.Fatalf("expected %d names but got %d", len(tc.expectedPdfEngines), len(mod.names))
}
for index, name := range mod.names {
if name != tc.expectPDFEngineNames[index] {
t.Errorf("expected name at index %d to be %s, but got: %s", index, name, tc.expectPDFEngineNames[index])
if name != tc.expectedPdfEngines[index] {
t.Fatalf("expected scenario at index %d to be %s, but got: %s", index, name, tc.expectedPdfEngines[index])
}
}
})
}
}
func TestPDFEngines_Validate(t *testing.T) {
tests := []struct {
name string
func TestPdfEngines_Validate(t *testing.T) {
for _, tc := range []struct {
scenario string
names []string
engines []gotenberg.PDFEngine
expectValidateErr bool
engines []gotenberg.PdfEngine
expectError bool
}{
{
name: "existing PDF engine",
scenario: "existing PDF engine",
names: []string{"foo"},
engines: func() []gotenberg.PDFEngine {
engines: func() []gotenberg.PdfEngine {
engine := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
}
return []gotenberg.PDFEngine{
return []gotenberg.PdfEngine{
engine,
}
}(),
expectError: false,
},
{
name: "non-existing bar PDF engine",
scenario: "non-existing bar PDF engine",
names: []string{"foo", "bar", "baz"},
engines: func() []gotenberg.PDFEngine {
engines: func() []gotenberg.PdfEngine {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
@@ -319,67 +321,65 @@ func TestPDFEngines_Validate(t *testing.T) {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
}
return []gotenberg.PDFEngine{
return []gotenberg.PdfEngine{
engine1,
engine2,
}
}(),
expectValidateErr: true,
expectError: true,
},
{
name: "no PDF engine",
expectValidateErr: true,
scenario: "no PDF engine",
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
mod := PDFEngines{
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := PdfEngines{
names: tc.names,
engines: tc.engines,
}
err := mod.Validate()
if tc.expectValidateErr && err == nil {
t.Errorf("expected mod.Validate() error, but got none")
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectValidateErr && err != nil {
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestPDFEngines_SystemMessages(t *testing.T) {
mod := new(PDFEngines)
func TestPdfEngines_SystemMessages(t *testing.T) {
mod := new(PdfEngines)
mod.names = []string{"foo", "bar"}
messages := mod.SystemMessages()
if len(messages) != 1 {
t.Errorf("expected one and only one message from mod.SystemMessages(), but got %d", len(messages))
t.Errorf("expected one and only one message, but got %d", len(messages))
}
expect := strings.Join(mod.names[:], " ")
if messages[0] != expect {
t.Errorf("expected message '%s' from mod.SystemMessages(), but got '%s'", expect, messages[0])
t.Errorf("expected message '%s', but got '%s'", expect, messages[0])
}
}
func TestPDFEngines_PDFEngine(t *testing.T) {
mod := PDFEngines{
func TestPdfEngines_PdfEngine(t *testing.T) {
mod := PdfEngines{
names: []string{"foo", "bar"},
engines: func() []gotenberg.PDFEngine {
engines: func() []gotenberg.PdfEngine {
engine1 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
@@ -387,57 +387,53 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
engine2 := &struct {
gotenberg.ModuleMock
gotenberg.PDFEngineMock
gotenberg.PdfEngineMock
}{}
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
}
return []gotenberg.PDFEngine{
return []gotenberg.PdfEngine{
engine1,
engine2,
}
}(),
}
_, err := mod.PDFEngine()
_, err := mod.PdfEngine()
if err != nil {
t.Errorf("expected no error from mod.PDFEngine, but got: %v", err)
t.Errorf("expected no error but got: %v", err)
}
}
func TestPDFEngines_Routes(t *testing.T) {
tests := []struct {
name string
mod PDFEngines
expectRoutesCount int
func TestPdfEngines_Routes(t *testing.T) {
for _, tc := range []struct {
scenario string
expectRoutes int
disableRoutes bool
}{
{
name: "route not disabled",
mod: PDFEngines{
engines: []gotenberg.PDFEngine{
&gotenberg.PDFEngineMock{},
},
},
expectRoutesCount: 2,
scenario: "routes not disabled",
expectRoutes: 2,
disableRoutes: false,
},
{
name: "route disabled",
mod: PDFEngines{
scenario: "routes disabled",
expectRoutes: 0,
disableRoutes: true,
},
},
}
} {
t.Run(tc.scenario, func(t *testing.T) {
mod := new(PdfEngines)
mod.disableRoutes = tc.disableRoutes
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
routes, err := tc.mod.Routes()
routes, err := mod.Routes()
if err != nil {
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectRoutesCount != len(routes) {
t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
if tc.expectRoutes != len(routes) {
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
}
})
}

View File

@@ -11,8 +11,8 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
)
// mergeRoute returns an api.Route which can merge PDFs.
func mergeRoute(engine gotenberg.PDFEngine) api.Route {
// mergeRoute returns an [api.Route] which can merge PDFs.
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/merge",
@@ -23,17 +23,38 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
// Let's get the data from the form and validate them.
var (
inputPaths []string
PDFformat string
pdfFormat string
pdfa string
pdfua bool
)
err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfFormat", &PDFformat, "").
String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
var actualPdfArchive string
if pdfFormat != "" {
// FIXME: deprecated
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = pdfFormat
}
if pdfa != "" {
actualPdfArchive = pdfa
}
pdfFormats := gotenberg.PdfFormats{
PdfA: actualPdfArchive,
PdfUa: pdfua,
}
// Alright, let's merge the PDFs.
outputPath := ctx.GeneratePath(".pdf")
@@ -45,21 +66,21 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
// So far so good, the PDFs are merged into one unique PDF.
// Now, let's check if the client want to convert this result PDF
// to a specific PDF format.
if PDFformat != "" {
// to specific PDF 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),
),
)
}
@@ -84,9 +105,9 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
}
}
// convertRoute returns an api.Route which can convert a PDF to a specific PDF
// format.
func convertRoute(engine gotenberg.PDFEngine) api.Route {
// convertRoute returns an [api.Route] which can convert a PDF to a specific
// PDF format.
func convertRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{
Method: http.MethodPost,
Path: "/forms/pdfengines/convert",
@@ -97,33 +118,64 @@ func convertRoute(engine gotenberg.PDFEngine) api.Route {
// Let's get the data from the form and validate them.
var (
inputPaths []string
PDFformat string
pdfFormat string
pdfa string
pdfua bool
)
err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("pdfFormat", &PDFformat).
String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate()
if err != nil {
return fmt.Errorf("validate form data: %w", err)
}
// Alright, let's merge the PDFs.
var actualPdfArchive string
if pdfFormat != "" {
// FIXME: deprecated.
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
actualPdfArchive = pdfFormat
}
if pdfa != "" {
actualPdfArchive = pdfa
}
pdfFormats := gotenberg.PdfFormats{
PdfA: actualPdfArchive,
PdfUa: pdfua,
}
zeroValued := gotenberg.PdfFormats{}
if pdfFormats == zeroValued {
return api.WrapError(
errors.New("no PDF formats"),
api.NewSentinelHTTPError(
http.StatusBadRequest,
"Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided",
),
)
}
// Alright, let's convert the PDFs.
outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), PDFformat, inputPath, outputPaths[i])
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
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

@@ -14,385 +14,408 @@ import (
)
func TestMergeHandler(t *testing.T) {
tests := []struct {
name string
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
engine gotenberg.PDFEngine
expectErr bool
expectHTTPErr bool
expectHTTPStatus int
engine gotenberg.PdfEngine
expectError bool
expectHttpError bool
expectHttpStatus int
expectOutputPathsCount int
}{
{
name: "nominal behavior",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 1,
scenario: "missing at least one mandatory file",
ctx: &api.ContextMock{Context: new(api.Context)},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
name: "invalid form data: no PDF",
ctx: &api.ContextMock{Context: &api.Context{}},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "merge fail",
scenario: "error from PDF engine",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
name: "nominal behavior with a PDF format",
scenario: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 1,
},
{
name: "convert to PDF format fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true,
},
{
name: "invalid PDF format",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
},
},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetCancelled(true)
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
{
scenario: "success",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "ErrPdfFormatNotSupported",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return gotenberg.ErrPdfFormatNotSupported
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "error from PDF engine (convert)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
scenario: "success with every PDF/A & PDF/UA form fields",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
return nil
},
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
c := echo.New().NewContext(nil, nil)
c.Set("context", tc.ctx.Context)
err := mergeRoute(tc.engine).Handler(c)
if tc.expectErr && err == nil {
t.Fatal("expected error from merge handler, but got none")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectErr && err != nil {
t.Fatalf("expected no error from merge handler, but got: %v", err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("expected HTTP error from merge handler, but got: %v", err)
if tc.expectHttpError && !isHTTPErr {
t.Errorf("expected an HTTP error but got: %v", err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("expected no HTTP error from merge handler, but got one: %v", httpErr)
if !tc.expectHttpError && isHTTPErr {
t.Errorf("expected no HTTP error but got one: %v", httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
if err != nil && tc.expectHttpError && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("expected %d HTTP status code from merge handler, but got %d", tc.expectHTTPStatus, status)
if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
}
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths from merge handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
}
})
}
}
func TestConvertHandler(t *testing.T) {
tests := []struct {
name string
for _, tc := range []struct {
scenario string
ctx *api.ContextMock
engine gotenberg.PDFEngine
expectErr bool
expectHTTPErr bool
expectHTTPStatus int
engine gotenberg.PdfEngine
expectError bool
expectHttpError bool
expectHttpStatus int
expectOutputPathsCount int
}{
{
name: "nominal behavior",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 1,
scenario: "missing at least one mandatory file",
ctx: &api.ContextMock{Context: new(api.Context)},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
name: "nominal behavior, but with 3 PDFs",
scenario: "no PDF formats",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"bar.pdf": "/bar/bar.pdf",
"baz.pdf": "/baz/baz.pdf",
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return nil
},
},
expectOutputPathsCount: 3,
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
name: "invalid form data: no PDF",
scenario: "ErrPdfFormatNotSupported",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
},
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "invalid form data: no PDF format",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
return ctx
}(),
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "convert to PDF format fail",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
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 gotenberg.ErrPdfFormatNotSupported
},
},
expectError: true,
expectHttpError: true,
expectHttpStatus: http.StatusBadRequest,
expectOutputPathsCount: 0,
},
{
scenario: "error from PDF engine",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfa": {
gotenberg.PdfA1a,
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return errors.New("foo")
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
{
name: "PDF format not available",
scenario: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
"foo",
},
})
return ctx
}(),
engine: &gotenberg.PDFEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
return gotenberg.ErrPDFFormatNotAvailable
},
},
expectErr: true,
expectHTTPErr: true,
expectHTTPStatus: http.StatusBadRequest,
},
{
name: "cannot add output paths",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: &api.Context{}}
ctx.SetFiles(map[string]string{
"foo.pdf": "/foo/foo.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.FormatPDFA1a,
"pdfa": {
gotenberg.PdfA1a,
},
})
ctx.SetCancelled(true)
return ctx
}(),
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
},
},
expectErr: true,
expectError: true,
expectHttpError: false,
expectOutputPathsCount: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
{
scenario: "success with every PDF/A & PDF/UA form fields (single file)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 1,
},
{
scenario: "success with every PDF/A & PDF/UA form fields (many files)",
ctx: func() *api.ContextMock {
ctx := &api.ContextMock{Context: new(api.Context)}
ctx.SetFiles(map[string]string{
"file.pdf": "/file.pdf",
"file2.pdf": "/file2.pdf",
})
ctx.SetValues(map[string][]string{
"pdfFormat": {
gotenberg.PdfA1a,
},
"pdfa": {
gotenberg.PdfA1a,
},
"pdfua": {
"true",
},
})
return ctx
}(),
engine: &gotenberg.PdfEngineMock{
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return nil
},
},
expectError: false,
expectHttpError: false,
expectOutputPathsCount: 2,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
tc.ctx.SetLogger(zap.NewNop())
c := echo.New().NewContext(nil, nil)
c.Set("context", tc.ctx.Context)
err := convertRoute(tc.engine).Handler(c)
if tc.expectErr && err == nil {
t.Fatal("expected error from convert handler, but got none")
if tc.expectError && err == nil {
t.Fatal("expected error but got none", err)
}
if !tc.expectErr && err != nil {
t.Fatalf("expected no error from convert handler, but got: %v", err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
var httpErr api.HTTPError
isHTTPErr := errors.As(err, &httpErr)
if tc.expectHTTPErr && !isHTTPErr {
t.Errorf("expected HTTP error from convert handler, but got: %v", err)
if tc.expectHttpError && !isHTTPErr {
t.Errorf("expected an HTTP error but got: %v", err)
}
if !tc.expectHTTPErr && isHTTPErr {
t.Errorf("expected no HTTP error from convert handler, but got one: %v", httpErr)
if !tc.expectHttpError && isHTTPErr {
t.Errorf("expected no HTTP error but got one: %v", httpErr)
}
if err != nil && tc.expectHTTPErr && isHTTPErr {
if err != nil && tc.expectHttpError && isHTTPErr {
status, _ := httpErr.HTTPError()
if status != tc.expectHTTPStatus {
t.Errorf("expected %d HTTP status code from convert handler, but got %d", tc.expectHTTPStatus, status)
if status != tc.expectHttpStatus {
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
}
}
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
t.Errorf("expected %d output paths from convert handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
}
})
}

View File

@@ -1,3 +1,8 @@
// Package pdftk provides a module which abstracts the CLI tool PDFtk and
// implements the gotenberg.PDFEngine interface.
// Package pdftk provides an implementation of the gotenberg.PdfEngine
// interface using the PDFtk command-line tool. This package allows for the
// merging of PDF files but does not support conversion to specific PDF
// formats. The path to the PDFtk binary must be specified using the
// PDFTK_BIN_PATH environment variable.
//
// See: https://gitlab.com/pdftk-java/pdftk.
package pdftk

View File

@@ -13,26 +13,25 @@ import (
)
func init() {
gotenberg.MustRegisterModule(PDFtk{})
gotenberg.MustRegisterModule(new(PdfTk))
}
// PDFtk abstracts the CLI tool PDFtk and implements the gotenberg.PDFEngine
// PdfTk abstracts the CLI tool PDFtk and implements the [gotenberg.PdfEngine]
// interface.
type PDFtk struct {
type PdfTk struct {
binPath string
}
// Descriptor returns a PDFtk's module descriptor.
func (PDFtk) Descriptor() gotenberg.ModuleDescriptor {
// Descriptor returns a [PdfTk]'s module descriptor.
func (engine *PdfTk) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "pdftk",
New: func() gotenberg.Module { return new(PDFtk) },
New: func() gotenberg.Module { return new(PdfTk) },
}
}
// Provision sets the modules properties. It returns an error if the
// environment variable PDFTK_BIN_PATH is not set.
func (engine *PDFtk) Provision(_ *gotenberg.Context) error {
// Provision sets the modules properties.
func (engine *PdfTk) Provision(ctx *gotenberg.Context) error {
binPath, ok := os.LookupEnv("PDFTK_BIN_PATH")
if !ok {
return errors.New("PDFTK_BIN_PATH environment variable is not set")
@@ -44,21 +43,22 @@ func (engine *PDFtk) Provision(_ *gotenberg.Context) error {
}
// Validate validates the module properties.
func (engine PDFtk) Validate() error {
func (engine *PdfTk) Validate() error {
_, err := os.Stat(engine.binPath)
if os.IsNotExist(err) {
return fmt.Errorf("PDFtk binary path does not exist: %w", err)
return fmt.Errorf("PdfTk binary path does not exist: %w", err)
}
return nil
}
// Metrics returns the metrics.
func (engine PDFtk) Metrics() ([]gotenberg.Metric, error) {
func (engine *PdfTk) Metrics() ([]gotenberg.Metric, error) {
// TODO: remove deprecated.
return []gotenberg.Metric{
{
Name: "pdftk_active_instances_count",
Description: "Current number of active PDFtk instances.",
Description: "Current number of active PDFtk instances - deprecated.",
Read: func() float64 {
activeInstancesCountMu.RLock()
defer activeInstancesCountMu.RUnlock()
@@ -69,8 +69,8 @@ func (engine PDFtk) Metrics() ([]gotenberg.Metric, error) {
}, nil
}
// Merge merges the given PDFs into a unique PDF.
func (engine PDFtk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
// Merge combines multiple PDFs into a single PDF.
func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
var args []string
args = append(args, inputPaths...)
args = append(args, "cat", "output", outputPath)
@@ -97,9 +97,9 @@ func (engine PDFtk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
return fmt.Errorf("merge PDFs with PDFtk: %w", err)
}
// Convert is not available for this PDF engine.
func (engine PDFtk) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
return fmt.Errorf("convert PDF to '%s' with PDFtk: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
// Convert is not available in this implementation.
func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
}
var (
@@ -109,9 +109,9 @@ var (
// Interface guards.
var (
_ gotenberg.Module = (*PDFtk)(nil)
_ gotenberg.Provisioner = (*PDFtk)(nil)
_ gotenberg.Validator = (*PDFtk)(nil)
_ gotenberg.MetricsProvider = (*PDFtk)(nil)
_ gotenberg.PDFEngine = (*PDFtk)(nil)
_ gotenberg.Module = (*PdfTk)(nil)
_ gotenberg.Provisioner = (*PdfTk)(nil)
_ gotenberg.Validator = (*PdfTk)(nil)
_ gotenberg.MetricsProvider = (*PdfTk)(nil)
_ gotenberg.PdfEngine = (*PdfTk)(nil)
)

View File

@@ -12,59 +12,67 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestPDFtk_Descriptor(t *testing.T) {
descriptor := PDFtk{}.Descriptor()
func TestPdfTk_Descriptor(t *testing.T) {
descriptor := new(PdfTk).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(PDFtk))
expect := reflect.TypeOf(new(PdfTk))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestPDFtk_Provision(t *testing.T) {
mod := new(PDFtk)
func TestPdfTk_Provision(t *testing.T) {
engine := new(PdfTk)
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
err := mod.Provision(ctx)
err := engine.Provision(ctx)
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
func TestPDFtk_Validate(t *testing.T) {
for i, tc := range []struct {
func TestPdfTk_Validate(t *testing.T) {
for _, tc := range []struct {
scenario string
binPath string
expectErr bool
expectError bool
}{
{
expectErr: true,
scenario: "empty bin path",
binPath: "",
expectError: true,
},
{
scenario: "bin path does not exist",
binPath: "/foo",
expectErr: true,
expectError: true,
},
{
scenario: "validate success",
binPath: os.Getenv("PDFTK_BIN_PATH"),
expectError: false,
},
} {
mod := new(PDFtk)
mod.binPath = tc.binPath
err := mod.Validate()
t.Run(tc.scenario, func(t *testing.T) {
engine := new(PdfTk)
engine.binPath = tc.binPath
err := engine.Validate()
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestPDFtk_Metrics(t *testing.T) {
metrics, err := new(PDFtk).Metrics()
func TestPdfTk_Metrics(t *testing.T) {
metrics, err := new(PdfTk).Metrics()
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
@@ -79,76 +87,80 @@ func TestPDFtk_Metrics(t *testing.T) {
}
}
func TestPDFtk_Merge(t *testing.T) {
for i, tc := range []struct {
func TestPdfTk_Merge(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx context.Context
inputPaths []string
expectErr bool
expectError bool
}{
{
scenario: "invalid context",
ctx: nil,
expectError: true,
},
{
scenario: "invalid input path",
ctx: context.TODO(),
inputPaths: []string{
"foo",
},
expectError: true,
},
{
scenario: "single file success",
ctx: context.TODO(),
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
},
},
{
scenario: "many files success",
ctx: context.TODO(),
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
"/tests/test/testdata/pdfengines/sample2.pdf",
},
},
{
ctx: nil,
expectErr: true,
},
{
ctx: context.TODO(),
inputPaths: []string{
"foo",
},
expectErr: true,
},
} {
func() {
mod := new(PDFtk)
err := mod.Provision(nil)
t.Run(tc.scenario, func(t *testing.T) {
engine := new(PdfTk)
err := engine.Provision(nil)
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
t.Fatalf("expected error but got: %v", err)
}
fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
t.Fatalf("expected error but got: %v", err)
}
defer func() {
err := os.RemoveAll(fs.WorkingDirPath())
err = os.RemoveAll(fs.WorkingDirPath())
if err != nil {
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
t.Fatalf("expected no error while cleaning up but got: %v", err)
}
}()
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
}()
})
}
}
func TestPDFtk_Convert(t *testing.T) {
mod := new(PDFtk)
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
func TestPdfTk_Convert(t *testing.T) {
engine := new(PdfTk)
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
}
}

View File

@@ -1,3 +1,8 @@
// Package qpdf provides a module which abstracts the CLI tool QPDF and
// implements the gotenberg.PDFEngine interface.
// Package qpdf provides an implementation of the gotenberg.PdfEngine
// interface using the QPDF command-line tool. This package allows for the
// merging of PDF files but does not support conversion to specific PDF
// formats. The path to the QPDF binary must be specified using the
// QPDK_BIN_PATH environment variable.
//
// See: https://github.com/qpdf/qpdf.
package qpdf

View File

@@ -13,26 +13,25 @@ import (
)
func init() {
gotenberg.MustRegisterModule(QPDF{})
gotenberg.MustRegisterModule(new(QPdf))
}
// QPDF abstracts the CLI tool QPDF and implements the gotenberg.PDFEngine
// QPdf abstracts the CLI tool QPDF and implements the [gotenberg.PdfEngine]
// interface.
type QPDF struct {
type QPdf struct {
binPath string
}
// Descriptor returns a QPDF's module descriptor.
func (QPDF) Descriptor() gotenberg.ModuleDescriptor {
// Descriptor returns a [QPdf]'s module descriptor.
func (engine *QPdf) Descriptor() gotenberg.ModuleDescriptor {
return gotenberg.ModuleDescriptor{
ID: "qpdf",
New: func() gotenberg.Module { return new(QPDF) },
New: func() gotenberg.Module { return new(QPdf) },
}
}
// Provision sets the modules properties. It returns an error if the
// environment variable QPDF_BIN_PATH is not set.
func (engine *QPDF) Provision(_ *gotenberg.Context) error {
// Provision sets the modules properties.
func (engine *QPdf) Provision(ctx *gotenberg.Context) error {
binPath, ok := os.LookupEnv("QPDF_BIN_PATH")
if !ok {
return errors.New("QPDF_BIN_PATH environment variable is not set")
@@ -44,21 +43,22 @@ func (engine *QPDF) Provision(_ *gotenberg.Context) error {
}
// Validate validates the module properties.
func (engine QPDF) Validate() error {
func (engine *QPdf) Validate() error {
_, err := os.Stat(engine.binPath)
if os.IsNotExist(err) {
return fmt.Errorf("QPDF binary path does not exist: %w", err)
return fmt.Errorf("QPdf binary path does not exist: %w", err)
}
return nil
}
// Metrics returns the metrics.
func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
func (engine *QPdf) Metrics() ([]gotenberg.Metric, error) {
// TODO: remove deprecated.
return []gotenberg.Metric{
{
Name: "qpdf_active_instances_count",
Description: "Current number of active QPDF instances.",
Description: "Current number of active QPDF instances - deprecated.",
Read: func() float64 {
activeInstancesCountMu.RLock()
defer activeInstancesCountMu.RUnlock()
@@ -69,8 +69,8 @@ func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
}, nil
}
// Merge merges the given PDFs into a unique PDF.
func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
// Merge combines multiple PDFs into a single PDF.
func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
var args []string
args = append(args, "--empty")
args = append(args, "--pages")
@@ -99,9 +99,9 @@ func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []s
return fmt.Errorf("merge PDFs with QPDF: %w", err)
}
// Convert is not available for this PDF engine.
func (engine QPDF) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
return fmt.Errorf("convert PDF to '%s' with QPDF: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
// Convert is not available in this implementation.
func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
}
var (
@@ -110,9 +110,9 @@ var (
)
var (
_ gotenberg.Module = (*QPDF)(nil)
_ gotenberg.Provisioner = (*QPDF)(nil)
_ gotenberg.Validator = (*QPDF)(nil)
_ gotenberg.MetricsProvider = (*QPDF)(nil)
_ gotenberg.PDFEngine = (*QPDF)(nil)
_ gotenberg.Module = (*QPdf)(nil)
_ gotenberg.Provisioner = (*QPdf)(nil)
_ gotenberg.Validator = (*QPdf)(nil)
_ gotenberg.MetricsProvider = (*QPdf)(nil)
_ gotenberg.PdfEngine = (*QPdf)(nil)
)

View File

@@ -12,59 +12,67 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
)
func TestQPDF_Descriptor(t *testing.T) {
descriptor := QPDF{}.Descriptor()
func TestQPdf_Descriptor(t *testing.T) {
descriptor := new(QPdf).Descriptor()
actual := reflect.TypeOf(descriptor.New())
expect := reflect.TypeOf(new(QPDF))
expect := reflect.TypeOf(new(QPdf))
if actual != expect {
t.Errorf("expected '%s' but got '%s'", expect, actual)
}
}
func TestQPDF_Provision(t *testing.T) {
mod := new(QPDF)
func TestQPdf_Provision(t *testing.T) {
engine := new(QPdf)
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
err := mod.Provision(ctx)
err := engine.Provision(ctx)
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
}
func TestQPDF_Validate(t *testing.T) {
for i, tc := range []struct {
func TestQPdf_Validate(t *testing.T) {
for _, tc := range []struct {
scenario string
binPath string
expectErr bool
expectError bool
}{
{
expectErr: true,
scenario: "empty bin path",
binPath: "",
expectError: true,
},
{
scenario: "bin path does not exist",
binPath: "/foo",
expectErr: true,
expectError: true,
},
{
scenario: "validate success",
binPath: os.Getenv("QPDF_BIN_PATH"),
expectError: false,
},
} {
mod := new(QPDF)
mod.binPath = tc.binPath
err := mod.Validate()
t.Run(tc.scenario, func(t *testing.T) {
engine := new(QPdf)
engine.binPath = tc.binPath
err := engine.Validate()
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
})
}
}
func TestQPDF_Metrics(t *testing.T) {
metrics, err := new(QPDF).Metrics()
func TestQPdf_Metrics(t *testing.T) {
metrics, err := new(QPdf).Metrics()
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
@@ -75,80 +83,84 @@ func TestQPDF_Metrics(t *testing.T) {
actual := metrics[0].Read()
if actual != 0 {
t.Errorf("expected %d QPDF instances, but got %f", 0, actual)
t.Errorf("expected %d QPdf instances, but got %f", 0, actual)
}
}
func TestQPDF_Merge(t *testing.T) {
for i, tc := range []struct {
func TestQPdf_Merge(t *testing.T) {
for _, tc := range []struct {
scenario string
ctx context.Context
inputPaths []string
expectErr bool
expectError bool
}{
{
scenario: "invalid context",
ctx: nil,
expectError: true,
},
{
scenario: "invalid input path",
ctx: context.TODO(),
inputPaths: []string{
"foo",
},
expectError: true,
},
{
scenario: "single file success",
ctx: context.TODO(),
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
},
},
{
scenario: "many files success",
ctx: context.TODO(),
inputPaths: []string{
"/tests/test/testdata/pdfengines/sample1.pdf",
"/tests/test/testdata/pdfengines/sample2.pdf",
},
},
{
ctx: nil,
expectErr: true,
},
{
ctx: context.TODO(),
inputPaths: []string{
"foo",
},
expectErr: true,
},
} {
func() {
mod := new(QPDF)
err := mod.Provision(nil)
t.Run(tc.scenario, func(t *testing.T) {
engine := new(QPdf)
err := engine.Provision(nil)
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
t.Fatalf("expected error but got: %v", err)
}
fs := gotenberg.NewFileSystem()
outputDir, err := fs.MkdirAll()
if err != nil {
t.Fatalf("test %d: expected error but got: %v", i, err)
t.Fatalf("expected error but got: %v", err)
}
defer func() {
err := os.RemoveAll(fs.WorkingDirPath())
err = os.RemoveAll(fs.WorkingDirPath())
if err != nil {
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
t.Fatalf("expected no error while cleaning up but got: %v", err)
}
}()
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
if tc.expectErr && err == nil {
t.Errorf("test %d: expected error but got: %v", i, err)
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if !tc.expectErr && err != nil {
t.Errorf("test %d: expected no error but got: %v", i, err)
if tc.expectError && err == nil {
t.Fatal("expected error but got none")
}
}()
})
}
}
func TestQPDF_Convert(t *testing.T) {
mod := new(QPDF)
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
func TestQPdf_Convert(t *testing.T) {
engine := new(QPdf)
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
}
}