diff --git a/build/Dockerfile b/build/Dockerfile index eb17a55c..3f257c8e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -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. diff --git a/go.mod b/go.mod index 55d42853..47f787d3 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6e663944..4acb47c2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/gotenberg/mocks.go b/pkg/gotenberg/mocks.go index 43502c6d..8979d2e2 100644 --- a/pkg/gotenberg/mocks.go +++ b/pkg/gotenberg/mocks.go @@ -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) diff --git a/pkg/gotenberg/mocks_test.go b/pkg/gotenberg/mocks_test.go index 62301e2b..dedaf7b5 100644 --- a/pkg/gotenberg/mocks_test.go +++ b/pkg/gotenberg/mocks_test.go @@ -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) } } diff --git a/pkg/gotenberg/pdfengine.go b/pkg/gotenberg/pdfengine.go index be84109c..2ef04083 100644 --- a/pkg/gotenberg/pdfengine.go +++ b/pkg/gotenberg/pdfengine.go @@ -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) } diff --git a/pkg/modules/api/formdata.go b/pkg/modules/api/formdata.go index 5c45a246..2db2bd26 100644 --- a/pkg/modules/api/formdata.go +++ b/pkg/modules/api/formdata.go @@ -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), ) } diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index fd6f764a..8fc8826b 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -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 diff --git a/pkg/modules/chromium/chromium_test.go b/pkg/modules/chromium/chromium_test.go index 7e0f9956..f66156a0 100644 --- a/pkg/modules/chromium/chromium_test.go +++ b/pkg/modules/chromium/chromium_test.go @@ -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( diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 4c158ad1..39c06b3e 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -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), ), ) } diff --git a/pkg/modules/chromium/routes_test.go b/pkg/modules/chromium/routes_test.go index 649485c6..da552e2c 100644 --- a/pkg/modules/chromium/routes_test.go +++ b/pkg/modules/chromium/routes_test.go @@ -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) diff --git a/pkg/modules/libreoffice/api/api.go b/pkg/modules/libreoffice/api/api.go index 9eb64a91..415bc1f8 100644 --- a/pkg/modules/libreoffice/api/api.go +++ b/pkg/modules/libreoffice/api/api.go @@ -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 diff --git a/pkg/modules/libreoffice/api/libreoffice.go b/pkg/modules/libreoffice/api/libreoffice.go index 72f13d99..453421b1 100644 --- a/pkg/modules/libreoffice/api/libreoffice.go +++ b/pkg/modules/libreoffice/api/libreoffice.go @@ -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...) diff --git a/pkg/modules/libreoffice/api/libreoffice_test.go b/pkg/modules/libreoffice/api/libreoffice_test.go index 63ae7670..67642cfb 100644 --- a/pkg/modules/libreoffice/api/libreoffice_test.go +++ b/pkg/modules/libreoffice/api/libreoffice_test.go @@ -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, diff --git a/pkg/modules/libreoffice/libreoffice.go b/pkg/modules/libreoffice/libreoffice.go index c064e6f2..e7978ca8 100644 --- a/pkg/modules/libreoffice/libreoffice.go +++ b/pkg/modules/libreoffice/libreoffice.go @@ -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) } diff --git a/pkg/modules/libreoffice/libreoffice_test.go b/pkg/modules/libreoffice/libreoffice_test.go index 39c6f9d3..39ee3e18 100644 --- a/pkg/modules/libreoffice/libreoffice_test.go +++ b/pkg/modules/libreoffice/libreoffice_test.go @@ -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( diff --git a/pkg/modules/libreoffice/pdfengine/doc.go b/pkg/modules/libreoffice/pdfengine/doc.go index 9c5d114f..4d81673f 100644 --- a/pkg/modules/libreoffice/pdfengine/doc.go +++ b/pkg/modules/libreoffice/pdfengine/doc.go @@ -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 diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine.go b/pkg/modules/libreoffice/pdfengine/pdfengine.go index d26bc66c..9c41d4aa 100644 --- a/pkg/modules/libreoffice/pdfengine/pdfengine.go +++ b/pkg/modules/libreoffice/pdfengine/pdfengine.go @@ -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) ) diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine_test.go b/pkg/modules/libreoffice/pdfengine/pdfengine_test.go index 7394b5da..dd8d5e90 100644 --- a/pkg/modules/libreoffice/pdfengine/pdfengine_test.go +++ b/pkg/modules/libreoffice/pdfengine/pdfengine_test.go @@ -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) diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go index fb4718dd..74bef367 100644 --- a/pkg/modules/libreoffice/routes.go +++ b/pkg/modules/libreoffice/routes.go @@ -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), ), ) } diff --git a/pkg/modules/libreoffice/routes_test.go b/pkg/modules/libreoffice/routes_test.go index 9db8584f..602e972c 100644 --- a/pkg/modules/libreoffice/routes_test.go +++ b/pkg/modules/libreoffice/routes_test.go @@ -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 }, }, diff --git a/pkg/modules/logging/doc.go b/pkg/modules/logging/doc.go index 87d49410..1d04f60b 100644 --- a/pkg/modules/logging/doc.go +++ b/pkg/modules/logging/doc.go @@ -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 diff --git a/pkg/modules/logging/logging_test.go b/pkg/modules/logging/logging_test.go index 341c03ba..dff7ed1e 100644 --- a/pkg/modules/logging/logging_test.go +++ b/pkg/modules/logging/logging_test.go @@ -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)) diff --git a/pkg/modules/pdfcpu/doc.go b/pkg/modules/pdfcpu/doc.go index 1d7b0058..0ce40d41 100644 --- a/pkg/modules/pdfcpu/doc.go +++ b/pkg/modules/pdfcpu/doc.go @@ -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 diff --git a/pkg/modules/pdfcpu/pdfcpu.go b/pkg/modules/pdfcpu/pdfcpu.go index f1c04d90..1155b008 100644 --- a/pkg/modules/pdfcpu/pdfcpu.go +++ b/pkg/modules/pdfcpu/pdfcpu.go @@ -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) ) diff --git a/pkg/modules/pdfcpu/pdfcpu_test.go b/pkg/modules/pdfcpu/pdfcpu_test.go index af94d720..40c1c3ba 100644 --- a/pkg/modules/pdfcpu/pdfcpu_test.go +++ b/pkg/modules/pdfcpu/pdfcpu_test.go @@ -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 { - inputPaths []string - expectErr bool +func TestPdfCpu_Merge(t *testing.T) { + for _, tc := range []struct { + scenario string + inputPaths []string + 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) } } diff --git a/pkg/modules/pdfengines/doc.go b/pkg/modules/pdfengines/doc.go index 667344d4..a6d46cc5 100644 --- a/pkg/modules/pdfengines/doc.go +++ b/pkg/modules/pdfengines/doc.go @@ -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 diff --git a/pkg/modules/pdfengines/multi.go b/pkg/modules/pdfengines/multi.go index 8b025fb4..619dba3c 100644 --- a/pkg/modules/pdfengines/multi.go +++ b/pkg/modules/pdfengines/multi.go @@ -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) ) diff --git a/pkg/modules/pdfengines/multi_test.go b/pkg/modules/pdfengines/multi_test.go index 66d2511b..67f34b84 100644 --- a/pkg/modules/pdfengines/multi_test.go +++ b/pkg/modules/pdfengines/multi_test.go @@ -10,61 +10,63 @@ import ( "github.com/gotenberg/gotenberg/v7/pkg/gotenberg" ) -func TestMultiPDFEngines_Merge(t *testing.T) { - tests := []struct { - name string - engine *multiPDFEngines - ctx context.Context - expectMergeErr bool +func TestMultiPdfEngines_Merge(t *testing.T) { + for _, tc := range []struct { + scenario string + engine *multiPdfEngines + ctx context.Context + 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(), + 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(), + 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, + ctx: context.Background(), + 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 - ctx context.Context - expectConvertErr bool +func TestMultiPdfEngines_Convert(t *testing.T) { + for _, tc := range []struct { + scenario string + engine *multiPdfEngines + ctx context.Context + 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, + ctx: context.Background(), + 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{}, "", "") - 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.expectError && err != nil { + t.Fatalf("expected no error but got: %v", err) } - 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") } }) } diff --git a/pkg/modules/pdfengines/pdfengines.go b/pkg/modules/pdfengines/pdfengines.go index 41a0caf4..0b8954b9 100644 --- a/pkg/modules/pdfengines/pdfengines.go +++ b/pkg/modules/pdfengines/pdfengines.go @@ -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) ) diff --git a/pkg/modules/pdfengines/pdfengines_test.go b/pkg/modules/pdfengines/pdfengines_test.go index 96b0278b..1f785f3d 100644 --- a/pkg/modules/pdfengines/pdfengines_test.go +++ b/pkg/modules/pdfengines/pdfengines_test.go @@ -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 - ctx *gotenberg.Context - expectPDFEngineNames []string - expectProvisionErr bool +func TestPdfEngines_Provision(t *testing.T) { + for _, tc := range []struct { + scenario string + ctx *gotenberg.Context + 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 - names []string - engines []gotenberg.PDFEngine - expectValidateErr bool +func TestPdfEngines_Validate(t *testing.T) { + for _, tc := range []struct { + scenario string + names []string + engines []gotenberg.PdfEngine + expectError bool }{ { - name: "existing PDF engine", - names: []string{"foo"}, - engines: func() []gotenberg.PDFEngine { + scenario: "existing PDF engine", + names: []string{"foo"}, + 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", - names: []string{"foo", "bar", "baz"}, - engines: func() []gotenberg.PDFEngine { + scenario: "non-existing bar PDF engine", + names: []string{"foo", "bar", "baz"}, + 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{ - disableRoutes: true, - }, + 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)) } }) } diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index 336feaeb..f1a04302 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -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), ), ) } diff --git a/pkg/modules/pdfengines/routes_test.go b/pkg/modules/pdfengines/routes_test.go index cfde99d0..f209334a 100644 --- a/pkg/modules/pdfengines/routes_test.go +++ b/pkg/modules/pdfengines/routes_test.go @@ -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", + scenario: "missing at least one mandatory file", + ctx: &api.ContextMock{Context: new(api.Context)}, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusBadRequest, + expectOutputPathsCount: 0, + }, + { + 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", + "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 + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusBadRequest, + expectOutputPathsCount: 0, + }, + { + scenario: "ErrPdfFormatNotSupported", + 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 gotenberg.ErrPdfFormatNotSupported }, }, - expectOutputPathsCount: 1, + expectError: true, + expectHttpError: true, + expectHttpStatus: http.StatusBadRequest, + expectOutputPathsCount: 0, }, { - name: "nominal behavior, but with 3 PDFs", + 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", - "bar.pdf": "/bar/bar.pdf", - "baz.pdf": "/baz/baz.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 { - return nil - }, - }, - expectOutputPathsCount: 3, - }, - { - name: "invalid form data: no PDF", - 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.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", - }) - 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 { + 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())) } }) } diff --git a/pkg/modules/pdftk/doc.go b/pkg/modules/pdftk/doc.go index 759540dd..48992e92 100644 --- a/pkg/modules/pdftk/doc.go +++ b/pkg/modules/pdftk/doc.go @@ -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 diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index 65a99aa8..f3e2e2ff 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -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) ) diff --git a/pkg/modules/pdftk/pdftk_test.go b/pkg/modules/pdftk/pdftk_test.go index cd43b89e..3defe0a1 100644 --- a/pkg/modules/pdftk/pdftk_test.go +++ b/pkg/modules/pdftk/pdftk_test.go @@ -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 { - binPath string - expectErr bool +func TestPdfTk_Validate(t *testing.T) { + for _, tc := range []struct { + scenario string + binPath string + expectError bool }{ { - expectErr: true, + scenario: "empty bin path", + binPath: "", + expectError: true, }, { - binPath: "/foo", - expectErr: true, + scenario: "bin path does not exist", + binPath: "/foo", + expectError: true, }, { - binPath: os.Getenv("PDFTK_BIN_PATH"), + 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 { - ctx context.Context - inputPaths []string - expectErr bool +func TestPdfTk_Merge(t *testing.T) { + for _, tc := range []struct { + scenario string + ctx context.Context + inputPaths []string + expectError bool }{ { - ctx: context.TODO(), + 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", }, }, { - ctx: context.TODO(), + 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) } } diff --git a/pkg/modules/qpdf/doc.go b/pkg/modules/qpdf/doc.go index c0c59c3e..4c90b711 100644 --- a/pkg/modules/qpdf/doc.go +++ b/pkg/modules/qpdf/doc.go @@ -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 diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index 856b444b..23a0ebcb 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -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) ) diff --git a/pkg/modules/qpdf/qpdf_test.go b/pkg/modules/qpdf/qpdf_test.go index b3ab45b3..34199230 100644 --- a/pkg/modules/qpdf/qpdf_test.go +++ b/pkg/modules/qpdf/qpdf_test.go @@ -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 { - binPath string - expectErr bool +func TestQPdf_Validate(t *testing.T) { + for _, tc := range []struct { + scenario string + binPath string + expectError bool }{ { - expectErr: true, + scenario: "empty bin path", + binPath: "", + expectError: true, }, { - binPath: "/foo", - expectErr: true, + scenario: "bin path does not exist", + binPath: "/foo", + expectError: true, }, { - binPath: os.Getenv("QPDF_BIN_PATH"), + 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 { - ctx context.Context - inputPaths []string - expectErr bool +func TestQPdf_Merge(t *testing.T) { + for _, tc := range []struct { + scenario string + ctx context.Context + inputPaths []string + expectError bool }{ { - ctx: context.TODO(), + 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", }, }, { - ctx: context.TODO(), + 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) } }