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

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

View File

@@ -69,10 +69,10 @@ RUN \
# Credits: # Credits:
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile. # 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. # 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 &&\ apt-get update -qq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \ 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 \ culmus \
fonts-beng \ fonts-beng \
fonts-hosny-amiri \ fonts-hosny-amiri \
@@ -107,7 +107,7 @@ RUN \
fonts-noto-ui-core \ fonts-noto-ui-core \
fonts-sil-gentium \ fonts-sil-gentium \
fonts-sil-gentium-basic &&\ 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. # Add Color and Black-and-White Noto emoji font.
# Credits: # Credits:
# https://github.com/gotenberg/gotenberg/pull/325. # https://github.com/gotenberg/gotenberg/pull/325.

12
go.mod
View File

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

23
go.sum
View File

@@ -10,8 +10,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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-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-20231101223124-24f5925b5980 h1:/nOO3ctHFqUmUwK5EdItjTCRQQCTcxWU7aJgyjFGtEc=
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/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 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA= github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= 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/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 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 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.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/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.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 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/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 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 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 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 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= 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/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 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 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-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-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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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= 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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=

View File

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

View File

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

View File

@@ -8,45 +8,74 @@ import (
) )
var ( var (
// ErrPDFEngineMethodNotAvailable happens if a PDFEngine method is not // ErrPdfEngineMethodNotSupported is returned when a specific method of the
// available in the implementation. // PdfEngine interface is not supported by its current implementation.
ErrPDFEngineMethodNotAvailable = errors.New("method not available") ErrPdfEngineMethodNotSupported = errors.New("method not supported")
// ErrPDFFormatNotAvailable happens if a PDFEngine Convert's method does // ErrPdfFormatNotSupported is returned when the Convert method of the
// not handle a specific format. // PdfEngine interface does not support a requested PDF format conversion.
ErrPDFFormatNotAvailable = errors.New("PDF format not available") ErrPdfFormatNotSupported = errors.New("PDF format not supported")
) )
const ( const (
FormatPDFA1a string = "PDF/A-1a" // PdfA1a represents the PDF/A-1a format.
FormatPDFA1b string = "PDF/A-1b" PdfA1a string = "PDF/A-1a"
FormatPDFA2a string = "PDF/A-2a"
FormatPDFA2b string = "PDF/A-2b" // PdfA1b represents the PDF/A-1b format.
FormatPDFA2u string = "PDF/A-2u" PdfA1b string = "PDF/A-1b"
FormatPDFA3a string = "PDF/A-3a"
FormatPDFA3b string = "PDF/A-3b" // PdfA2a represents the PDF/A-2a format.
FormatPDFA3u string = "PDF/A-3u" 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 // PdfFormats specifies the target formats for a PDF conversion.
// or more PDFs. Implementations may abstract powerful tools like PDFtk, or type PdfFormats struct {
// fulfill those methods contracts in Golang directly. // PdfA denotes the PDF/A standard format (e.g., PDF/A-1a).
type PDFEngine interface { PdfA string
// Merge merges the given PDFs into a unique PDF. The pages' order reflects
// order of the given files. // 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 Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
// Convert converts the given PDF to a specific PDF format. // Convert transforms a given PDF to the specified formats defined in
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error // 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 // PdfEngineProvider offers an interface to instantiate a [PdfEngine].
// PDFEngine for other modules. // This is used to decouple the creation of a [PdfEngine] from its consumers.
//
// Example:
// //
// func (m *YourModule) Provision(ctx *gotenberg.Context) error { // func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// provider, _ := ctx.Module(new(gotenberg.PDFEngineProvider)) // provider, _ := ctx.Module(new(gotenberg.PdfEngineProvider))
// pdfengines, _ := provider.(gotenberg.PDFEngineProvider).PDFEngine() // engine, _ := provider.(gotenberg.PdfEngineProvider).PdfEngine()
// } // }
type PDFEngineProvider interface { type PdfEngineProvider interface {
PDFEngine() (PDFEngine, error) // PdfEngine returns an instance of the PdfEngine interface for PDF operations.
PdfEngine() (PdfEngine, error)
} }

View File

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

View File

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

View File

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

View File

@@ -129,8 +129,41 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
return form, 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. // convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route { func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/chromium/convert/url", Path: "/forms/chromium/convert/url",
@@ -138,21 +171,17 @@ func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(ctx)
var ( var url string
url string
pdfFormat string
)
err := form. err := form.
MandatoryString("url", &url). MandatoryString("url", &url).
String("pdfFormat", &pdfFormat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) 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 { if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err) 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 // convertHtmlRoute returns an [api.Route] which can convert an HTML file to
// PDF. // PDF.
func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route { func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/chromium/convert/html", Path: "/forms/chromium/convert/html",
@@ -172,23 +201,18 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(ctx)
var ( var inputPath string
inputPath string
pdfFormat string
)
err := form. err := form.
MandatoryPath("index.html", &inputPath). MandatoryPath("index.html", &inputPath).
String("pdfFormat", &pdfFormat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
url := fmt.Sprintf("file://%s", inputPath) url := fmt.Sprintf("file://%s", inputPath)
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
if err != nil { if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err) 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 // convertMarkdownRoute returns an [api.Route] which can convert markdown files
// to PDF. // to PDF.
func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route { func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/chromium/convert/markdown", Path: "/forms/chromium/convert/markdown",
@@ -208,17 +232,16 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
Handler: func(c echo.Context) error { Handler: func(c echo.Context) error {
ctx := c.Get("context").(*api.Context) ctx := c.Get("context").(*api.Context)
form, options := FormDataChromiumPdfOptions(ctx) form, options := FormDataChromiumPdfOptions(ctx)
pdfFormats := FormDataChromiumPdfFormats(ctx)
var ( var (
inputPath string inputPath string
markdownPaths []string markdownPaths []string
pdfFormat string
) )
err := form. err := form.
MandatoryPath("index.html", &inputPath). MandatoryPath("index.html", &inputPath).
MandatoryPaths([]string{".md"}, &markdownPaths). MandatoryPaths([]string{".md"}, &markdownPaths).
String("pdfFormat", &pdfFormat, "").
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err) 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. // 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") outputPath := ctx.GeneratePath(".pdf")
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options) 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. // So far so good, the URL has been converted to PDF.
// Now, let's check if the client want to convert this result PDF // Now, let's check if the client want to convert the resulting PDF
// to a specific PDF format. // to specific formats.
zeroValued := gotenberg.PdfFormats{}
if pdfFormat != "" { if pdfFormats != zeroValued {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf") 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 err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHTTPError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", pdfFormat), fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
), ),
) )
} }

View File

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

View File

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

View File

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

View File

@@ -243,7 +243,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return p return p
}(), }(),
fs: gotenberg.NewFileSystem(), fs: gotenberg.NewFileSystem(),
options: Options{PdfFormat: "foo"}, options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}},
cancelledCtx: false, cancelledCtx: false,
start: false, start: false,
expectError: true, expectError: true,
@@ -417,7 +417,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return fs return fs
}(), }(),
options: Options{PdfFormat: gotenberg.FormatPDFA1a}, options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1a}},
cancelledCtx: false, cancelledCtx: false,
start: true, start: true,
expectError: false, expectError: false,
@@ -446,7 +446,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return fs return fs
}(), }(),
options: Options{PdfFormat: gotenberg.FormatPDFA2b}, options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA2b}},
cancelledCtx: false, cancelledCtx: false,
start: true, start: true,
expectError: false, expectError: false,
@@ -475,7 +475,36 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
return fs 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, cancelledCtx: false,
start: true, start: true,
expectError: false, expectError: false,

View File

@@ -18,7 +18,7 @@ func init() {
// PDF with LibreOffice. // PDF with LibreOffice.
type LibreOffice struct { type LibreOffice struct {
api libeofficeapi.Uno api libeofficeapi.Uno
engine gotenberg.PDFEngine engine gotenberg.PdfEngine
disableRoutes bool disableRoutes bool
} }
@@ -53,12 +53,12 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
mod.api = libreOfficeApi mod.api = libreOfficeApi
provider, err = ctx.Module(new(gotenberg.PDFEngineProvider)) provider, err = ctx.Module(new(gotenberg.PdfEngineProvider))
if err != nil { if err != nil {
return fmt.Errorf("get PDF engine provider: %w", err) return fmt.Errorf("get PDF engine provider: %w", err)
} }
engine, err := provider.(gotenberg.PDFEngineProvider).PDFEngine() engine, err := provider.(gotenberg.PdfEngineProvider).PdfEngine()
if err != nil { if err != nil {
return fmt.Errorf("get PDF engine: %w", err) return fmt.Errorf("get PDF engine: %w", err)
} }

View File

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

View File

@@ -1,4 +1,4 @@
// Package pdfengine provides a module which interacts with LibreOffice via the // 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. // interface.
package pdfengine package pdfengine

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,30 +10,24 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg" "github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
) )
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one type multiPdfEngines struct {
// or more gotenberg.PDFEngine. It provides a sort of fallback mechanism: if an engines []gotenberg.PdfEngine
// engine's method returns an error, it calls the same method from another
// engine.
type multiPDFEngines struct {
engines []gotenberg.PDFEngine
} }
// newMultiPDFEngines returns a multiPDFEngines. Arguments' order determines the func newMultiPdfEngines(engines ...gotenberg.PdfEngine) *multiPdfEngines {
// order of the engines called. return &multiPdfEngines{
func newMultiPDFEngines(engines ...gotenberg.PDFEngine) *multiPDFEngines {
return &multiPDFEngines{
engines: engines, engines: engines,
} }
} }
// Merge tries to merge the given PDFs into a unique PDF thanks to its // 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. // 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 var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
for _, engine := range multi.engines { for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) { go func(engine gotenberg.PdfEngine) {
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath) errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
}(engine) }(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 // 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. // 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 var err error
errChan := make(chan error, 1) errChan := make(chan error, 1)
for _, engine := range multi.engines { for _, engine := range multi.engines {
go func(engine gotenberg.PDFEngine) { go func(engine gotenberg.PdfEngine) {
errChan <- engine.Convert(ctx, logger, format, inputPath, outputPath) errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
}(engine) }(engine)
select { 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. // Interface guards.
var ( var (
_ gotenberg.PDFEngine = (*multiPDFEngines)(nil) _ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
) )

View File

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

View File

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

View File

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

View File

@@ -11,8 +11,8 @@ import (
"github.com/gotenberg/gotenberg/v7/pkg/modules/api" "github.com/gotenberg/gotenberg/v7/pkg/modules/api"
) )
// mergeRoute returns an api.Route which can merge PDFs. // mergeRoute returns an [api.Route] which can merge PDFs.
func mergeRoute(engine gotenberg.PDFEngine) api.Route { func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/pdfengines/merge", 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. // Let's get the data from the form and validate them.
var ( var (
inputPaths []string inputPaths []string
PDFformat string pdfFormat string
pdfa string
pdfua bool
) )
err := ctx.FormData(). err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
String("pdfFormat", &PDFformat, ""). String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
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. // Alright, let's merge the PDFs.
outputPath := ctx.GeneratePath(".pdf") 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. // 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 // Now, let's check if the client want to convert this result PDF
// to a specific PDF format. // to specific PDF formats.
zeroValued := gotenberg.PdfFormats{}
if PDFformat != "" { if pdfFormats != zeroValued {
convertInputPath := outputPath convertInputPath := outputPath
convertOutputPath := ctx.GeneratePath(".pdf") 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 err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHTTPError(
http.StatusBadRequest, 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 // convertRoute returns an [api.Route] which can convert a PDF to a specific
// format. // PDF format.
func convertRoute(engine gotenberg.PDFEngine) api.Route { func convertRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
Method: http.MethodPost, Method: http.MethodPost,
Path: "/forms/pdfengines/convert", 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. // Let's get the data from the form and validate them.
var ( var (
inputPaths []string inputPaths []string
PDFformat string pdfFormat string
pdfa string
pdfua bool
) )
err := ctx.FormData(). err := ctx.FormData().
MandatoryPaths([]string{".pdf"}, &inputPaths). MandatoryPaths([]string{".pdf"}, &inputPaths).
MandatoryString("pdfFormat", &PDFformat). String("pdfFormat", &pdfFormat, "").
String("pdfa", &pdfa, "").
Bool("pdfua", &pdfua, false).
Validate() Validate()
if err != nil { if err != nil {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
// 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)) outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") outputPaths[i] = ctx.GeneratePath(".pdf")
err = engine.Convert(ctx, ctx.Log(), PDFformat, inputPath, outputPaths[i]) err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
if err != nil { if err != nil {
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) { if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
return api.WrapError( return api.WrapError(
fmt.Errorf("convert PDF: %w", err), fmt.Errorf("convert PDF: %w", err),
api.NewSentinelHTTPError( api.NewSentinelHTTPError(
http.StatusBadRequest, http.StatusBadRequest,
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat), fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
), ),
) )
} }

View File

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

View File

@@ -1,3 +1,8 @@
// Package pdftk provides a module which abstracts the CLI tool PDFtk and // Package pdftk provides an implementation of the gotenberg.PdfEngine
// implements the gotenberg.PDFEngine interface. // 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 package pdftk

View File

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

View File

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

View File

@@ -1,3 +1,8 @@
// Package qpdf provides a module which abstracts the CLI tool QPDF and // Package qpdf provides an implementation of the gotenberg.PdfEngine
// implements the gotenberg.PDFEngine interface. // 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 package qpdf

View File

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

View File

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