mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
feat(pdfengines): add PDF/UA (#714)
This commit is contained in:
@@ -69,10 +69,10 @@ RUN \
|
||||
# Credits:
|
||||
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
|
||||
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
|
||||
curl -o ./ttf-mscorefonts-installer_3.8_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8_all.deb &&\
|
||||
curl -o ./ttf-mscorefonts-installer_3.8.1_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||
apt-get update -qq &&\
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||
./ttf-mscorefonts-installer_3.8_all.deb \
|
||||
./ttf-mscorefonts-installer_3.8.1_all.deb \
|
||||
culmus \
|
||||
fonts-beng \
|
||||
fonts-hosny-amiri \
|
||||
@@ -107,7 +107,7 @@ RUN \
|
||||
fonts-noto-ui-core \
|
||||
fonts-sil-gentium \
|
||||
fonts-sil-gentium-basic &&\
|
||||
rm -f ./ttf-mscorefonts-installer_3.8_all.deb &&\
|
||||
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
|
||||
# Add Color and Black-and-White Noto emoji font.
|
||||
# Credits:
|
||||
# https://github.com/gotenberg/gotenberg/pull/325.
|
||||
|
||||
12
go.mod
12
go.mod
@@ -5,7 +5,7 @@ go 1.21
|
||||
require (
|
||||
github.com/alexliesenfeld/health v0.8.0
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/chromedp/cdproto v0.0.0-20231025043423-5615e204d422
|
||||
github.com/chromedp/cdproto v0.0.0-20231101223124-24f5925b5980
|
||||
github.com/chromedp/chromedp v0.9.3
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/uuid v1.4.0
|
||||
@@ -30,10 +30,10 @@ require (
|
||||
golang.org/x/crypto v0.14.0 // indirect
|
||||
golang.org/x/image v0.13.0 // indirect
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/sync v0.4.0
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/sync v0.5.0
|
||||
golang.org/x/sys v0.14.0 // indirect
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/text v0.13.0
|
||||
golang.org/x/text v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -44,8 +44,8 @@ require (
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.3.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gobwas/ws v1.3.1 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hhrutter/lzw v1.0.0 // indirect
|
||||
github.com/hhrutter/tiff v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
|
||||
23
go.sum
23
go.sum
@@ -10,8 +10,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/cdproto v0.0.0-20231025043423-5615e204d422 h1:9d05eR3+VAcQqH5qiKbpMFa83XFK7jVDfAXIindFPbU=
|
||||
github.com/chromedp/cdproto v0.0.0-20231025043423-5615e204d422/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/cdproto v0.0.0-20231101223124-24f5925b5980 h1:/nOO3ctHFqUmUwK5EdItjTCRQQCTcxWU7aJgyjFGtEc=
|
||||
github.com/chromedp/cdproto v0.0.0-20231101223124-24f5925b5980/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
||||
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||
@@ -26,8 +26,9 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
|
||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
@@ -37,8 +38,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
|
||||
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
|
||||
@@ -147,19 +148,19 @@ golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
|
||||
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
|
||||
@@ -24,27 +24,27 @@ func (mod *ValidatorMock) Validate() error {
|
||||
return mod.ValidateMock()
|
||||
}
|
||||
|
||||
// PDFEngineMock is a mock for the [PDFEngine] interface.
|
||||
type PDFEngineMock struct {
|
||||
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
||||
type PdfEngineMock struct {
|
||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
}
|
||||
|
||||
func (engine *PDFEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return engine.MergeMock(ctx, logger, inputPaths, outputPath)
|
||||
}
|
||||
|
||||
func (engine *PDFEngineMock) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return engine.ConvertMock(ctx, logger, format, inputPath, outputPath)
|
||||
func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||
}
|
||||
|
||||
// PDFEngineProviderMock is a mock for the [PDFEngineProvider] interface.
|
||||
type PDFEngineProviderMock struct {
|
||||
PDFEngineMock func() (PDFEngine, error)
|
||||
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
||||
type PdfEngineProviderMock struct {
|
||||
PdfEngineMock func() (PdfEngine, error)
|
||||
}
|
||||
|
||||
func (provider *PDFEngineProviderMock) PDFEngine() (PDFEngine, error) {
|
||||
return provider.PDFEngineMock()
|
||||
func (provider *PdfEngineProviderMock) PdfEngine() (PdfEngine, error) {
|
||||
return provider.PdfEngineMock()
|
||||
}
|
||||
|
||||
// ProcessMock is a mock for the [Process] interface.
|
||||
@@ -113,8 +113,8 @@ func (provider *LoggerProviderMock) Logger(mod Module) (*zap.Logger, error) {
|
||||
var (
|
||||
_ Module = (*ModuleMock)(nil)
|
||||
_ Validator = (*ValidatorMock)(nil)
|
||||
_ PDFEngine = (*PDFEngineMock)(nil)
|
||||
_ PDFEngineProvider = (*PDFEngineProviderMock)(nil)
|
||||
_ PdfEngine = (*PdfEngineMock)(nil)
|
||||
_ PdfEngineProvider = (*PdfEngineProviderMock)(nil)
|
||||
_ Process = (*ProcessMock)(nil)
|
||||
_ ProcessSupervisor = (*ProcessSupervisorMock)(nil)
|
||||
_ LoggerProvider = (*LoggerProviderMock)(nil)
|
||||
|
||||
@@ -35,36 +35,36 @@ func TestValidatorMock(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPDFEngineMock(t *testing.T) {
|
||||
mock := &PDFEngineMock{
|
||||
mock := &PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
err := mock.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PDFEngineMock.Merge, but got: %v", err)
|
||||
t.Errorf("expected no error from PdfEngineMock.Merge, but got: %v", err)
|
||||
}
|
||||
|
||||
err = mock.Convert(context.Background(), zap.NewNop(), "", "", "")
|
||||
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PDFEngineMock.Convert, but got: %v", err)
|
||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngineProviderMock(t *testing.T) {
|
||||
mock := &PDFEngineProviderMock{
|
||||
PDFEngineMock: func() (PDFEngine, error) {
|
||||
return new(PDFEngineMock), nil
|
||||
mock := &PdfEngineProviderMock{
|
||||
PdfEngineMock: func() (PdfEngine, error) {
|
||||
return new(PdfEngineMock), nil
|
||||
},
|
||||
}
|
||||
|
||||
_, err := mock.PDFEngine()
|
||||
_, err := mock.PdfEngine()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from PDFEngineProviderMock.PDFEngine, but got: %v", err)
|
||||
t.Errorf("expected no error from PdfEngineProviderMock.PdfEngine, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,45 +8,74 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPDFEngineMethodNotAvailable happens if a PDFEngine method is not
|
||||
// available in the implementation.
|
||||
ErrPDFEngineMethodNotAvailable = errors.New("method not available")
|
||||
// ErrPdfEngineMethodNotSupported is returned when a specific method of the
|
||||
// PdfEngine interface is not supported by its current implementation.
|
||||
ErrPdfEngineMethodNotSupported = errors.New("method not supported")
|
||||
|
||||
// ErrPDFFormatNotAvailable happens if a PDFEngine Convert's method does
|
||||
// not handle a specific format.
|
||||
ErrPDFFormatNotAvailable = errors.New("PDF format not available")
|
||||
// ErrPdfFormatNotSupported is returned when the Convert method of the
|
||||
// PdfEngine interface does not support a requested PDF format conversion.
|
||||
ErrPdfFormatNotSupported = errors.New("PDF format not supported")
|
||||
)
|
||||
|
||||
const (
|
||||
FormatPDFA1a string = "PDF/A-1a"
|
||||
FormatPDFA1b string = "PDF/A-1b"
|
||||
FormatPDFA2a string = "PDF/A-2a"
|
||||
FormatPDFA2b string = "PDF/A-2b"
|
||||
FormatPDFA2u string = "PDF/A-2u"
|
||||
FormatPDFA3a string = "PDF/A-3a"
|
||||
FormatPDFA3b string = "PDF/A-3b"
|
||||
FormatPDFA3u string = "PDF/A-3u"
|
||||
// PdfA1a represents the PDF/A-1a format.
|
||||
PdfA1a string = "PDF/A-1a"
|
||||
|
||||
// PdfA1b represents the PDF/A-1b format.
|
||||
PdfA1b string = "PDF/A-1b"
|
||||
|
||||
// PdfA2a represents the PDF/A-2a format.
|
||||
PdfA2a string = "PDF/A-2a"
|
||||
|
||||
// PdfA2b represents the PDF/A-2b format.
|
||||
PdfA2b string = "PDF/A-2b"
|
||||
|
||||
// PdfA2u represents the PDF/A-2u format.
|
||||
PdfA2u string = "PDF/A-2u"
|
||||
|
||||
// PdfA3a represents the PDF/A-3a format.
|
||||
PdfA3a string = "PDF/A-3a"
|
||||
|
||||
// PdfA3b represents the PDF/A-3b format.
|
||||
PdfA3b string = "PDF/A-3b"
|
||||
|
||||
// PdfA3u represents the PDF/A-3u format.
|
||||
PdfA3u string = "PDF/A-3u"
|
||||
)
|
||||
|
||||
// PDFEngine is a module interface which exposes methods for manipulating one
|
||||
// or more PDFs. Implementations may abstract powerful tools like PDFtk, or
|
||||
// fulfill those methods contracts in Golang directly.
|
||||
type PDFEngine interface {
|
||||
// Merge merges the given PDFs into a unique PDF. The pages' order reflects
|
||||
// order of the given files.
|
||||
// PdfFormats specifies the target formats for a PDF conversion.
|
||||
type PdfFormats struct {
|
||||
// PdfA denotes the PDF/A standard format (e.g., PDF/A-1a).
|
||||
PdfA string
|
||||
|
||||
// PdfUa indicates whether the PDF should comply
|
||||
// with the PDF/UA (Universal Accessibility) standard.
|
||||
PdfUa bool
|
||||
}
|
||||
|
||||
// PdfEngine provides an interface for operations on PDFs. Implementations
|
||||
// can utilize various tools like PDFtk, or implement functionality directly in
|
||||
// Go.
|
||||
type PdfEngine interface {
|
||||
// Merge combines multiple PDFs into a single PDF. The resulting page order
|
||||
// is determined by the order of files provided in inputPaths.
|
||||
Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format.
|
||||
Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error
|
||||
// Convert transforms a given PDF to the specified formats defined in
|
||||
// PdfFormats. If no format, it does nothing.
|
||||
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
}
|
||||
|
||||
// PDFEngineProvider is a module interface which exposes a method for creating a
|
||||
// PDFEngine for other modules.
|
||||
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
||||
// This is used to decouple the creation of a [PdfEngine] from its consumers.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
|
||||
// provider, _ := ctx.Module(new(gotenberg.PDFEngineProvider))
|
||||
// pdfengines, _ := provider.(gotenberg.PDFEngineProvider).PDFEngine()
|
||||
// provider, _ := ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||
// engine, _ := provider.(gotenberg.PdfEngineProvider).PdfEngine()
|
||||
// }
|
||||
type PDFEngineProvider interface {
|
||||
PDFEngine() (PDFEngine, error)
|
||||
type PdfEngineProvider interface {
|
||||
// PdfEngine returns an instance of the PdfEngine interface for PDF operations.
|
||||
PdfEngine() (PdfEngine, error)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func (form FormData) Validate() error {
|
||||
)
|
||||
}
|
||||
|
||||
// String binds a form data value to a string variable.
|
||||
// String binds a form field to a string variable.
|
||||
//
|
||||
// var foo string
|
||||
//
|
||||
@@ -52,17 +52,17 @@ func (form *FormData) String(key string, target *string, defaultValue string) *F
|
||||
return form.mustValue(key, target, defaultValue)
|
||||
}
|
||||
|
||||
// MandatoryString binds a form data value to a string variable. It populates
|
||||
// MandatoryString binds a form field to a string variable. It populates
|
||||
// an error if the value is empty or the "key" does not exist.
|
||||
//
|
||||
// var foo string
|
||||
//
|
||||
// ctx.FormData().MandatoryString("foo", &foo)
|
||||
func (form *FormData) MandatoryString(key string, target *string) *FormData {
|
||||
return form.mustMandatoryValue(key, target)
|
||||
return form.mustMandatoryField(key, target)
|
||||
}
|
||||
|
||||
// Bool binds a form data value to a bool variable. It populates an error if
|
||||
// Bool binds a form field to a bool variable. It populates an error if
|
||||
// the value is not bool.
|
||||
//
|
||||
// var foo bool
|
||||
@@ -72,17 +72,17 @@ func (form *FormData) Bool(key string, target *bool, defaultValue bool) *FormDat
|
||||
return form.mustValue(key, target, defaultValue)
|
||||
}
|
||||
|
||||
// MandatoryBool binds a form data value to a bool variable. It populates an
|
||||
// MandatoryBool binds a form field to a bool variable. It populates an
|
||||
// error if the value is not bool, is empty, or the "key" does not exist.
|
||||
//
|
||||
// var foo bool
|
||||
//
|
||||
// ctx.FormData().MandatoryBool("foo", &foo)
|
||||
func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
|
||||
return form.mustMandatoryValue(key, target)
|
||||
return form.mustMandatoryField(key, target)
|
||||
}
|
||||
|
||||
// Int binds a form data value to an int variable. It populates an error if the
|
||||
// Int binds a form field to an int variable. It populates an error if the
|
||||
// value is not int.
|
||||
//
|
||||
// var foo int
|
||||
@@ -92,17 +92,17 @@ func (form *FormData) Int(key string, target *int, defaultValue int) *FormData {
|
||||
return form.mustValue(key, target, defaultValue)
|
||||
}
|
||||
|
||||
// MandatoryInt binds a form data value to an int variable. It populates an
|
||||
// MandatoryInt binds a form field to an int variable. It populates an
|
||||
// error if the value is not int, is empty, or the "key" does not exist.
|
||||
//
|
||||
// var foo int
|
||||
//
|
||||
// ctx.FormData().MandatoryInt("foo", &foo)
|
||||
func (form *FormData) MandatoryInt(key string, target *int) *FormData {
|
||||
return form.mustMandatoryValue(key, target)
|
||||
return form.mustMandatoryField(key, target)
|
||||
}
|
||||
|
||||
// Float64 binds a form data value to a float64 variable. It populates an error
|
||||
// Float64 binds a form field to a float64 variable. It populates an error
|
||||
// if the value is not float64.
|
||||
//
|
||||
// var foo float64
|
||||
@@ -112,18 +112,18 @@ func (form *FormData) Float64(key string, target *float64, defaultValue float64)
|
||||
return form.mustValue(key, target, defaultValue)
|
||||
}
|
||||
|
||||
// MandatoryFloat64 binds a form data value to a float64 variable. It populates
|
||||
// MandatoryFloat64 binds a form field to a float64 variable. It populates
|
||||
// an error if the is not float64, is empty, or the "key" does not exist.
|
||||
//
|
||||
// var foo float64
|
||||
//
|
||||
// ctx.FormData().MandatoryFloat64("foo", &foo)
|
||||
func (form *FormData) MandatoryFloat64(key string, target *float64) *FormData {
|
||||
return form.mustMandatoryValue(key, target)
|
||||
return form.mustMandatoryField(key, target)
|
||||
}
|
||||
|
||||
// Duration binds a form data value to a time.Duration variable. It populates
|
||||
// an error if the form data value is not time.Duration.
|
||||
// Duration binds a form field to a time.Duration variable. It populates
|
||||
// an error if the form field is not time.Duration.
|
||||
//
|
||||
// var foo time.Duration
|
||||
//
|
||||
@@ -132,7 +132,7 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
|
||||
return form.mustValue(key, target, defaultValue)
|
||||
}
|
||||
|
||||
// MandatoryDuration binds a form data value to a time.Duration variable. It
|
||||
// MandatoryDuration binds a form field to a time.Duration variable. It
|
||||
// populates an error if the value is not time.Duration, is empty, or the "key"
|
||||
// does not exist.
|
||||
//
|
||||
@@ -140,10 +140,10 @@ func (form *FormData) Duration(key string, target *time.Duration, defaultValue t
|
||||
//
|
||||
// ctx.FormData().MandatoryDuration("foo", &foo)
|
||||
func (form *FormData) MandatoryDuration(key string, target *time.Duration) *FormData {
|
||||
return form.mustMandatoryValue(key, target)
|
||||
return form.mustMandatoryField(key, target)
|
||||
}
|
||||
|
||||
// Custom helps to define a custom binding function for a form data value.
|
||||
// Custom helps to define a custom binding function for a form field.
|
||||
//
|
||||
// var foo map[string]string
|
||||
//
|
||||
@@ -168,16 +168,15 @@ func (form *FormData) Custom(key string, assign func(value string) error) *FormD
|
||||
err := assign(value)
|
||||
if err != nil {
|
||||
form.append(
|
||||
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||
)
|
||||
}
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
// MandatoryCustom helps to define a custom binding function for a form data
|
||||
// value. It populates an error if the value is empty or the "key" does not
|
||||
// exist.
|
||||
// MandatoryCustom helps to define a custom binding function for a form field.
|
||||
// It populates an error if the value is empty or the "key" does not exist.
|
||||
//
|
||||
// var foo map[string]string
|
||||
//
|
||||
@@ -191,7 +190,7 @@ func (form *FormData) Custom(key string, assign func(value string) error) *FormD
|
||||
// })
|
||||
func (form *FormData) MandatoryCustom(key string, assign func(value string) error) *FormData {
|
||||
var value string
|
||||
form.mustMandatoryValue(key, &value)
|
||||
form.mustMandatoryField(key, &value)
|
||||
|
||||
if value == "" {
|
||||
return form
|
||||
@@ -200,7 +199,7 @@ func (form *FormData) MandatoryCustom(key string, assign func(value string) erro
|
||||
err := assign(value)
|
||||
if err != nil {
|
||||
form.append(
|
||||
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -315,7 +314,7 @@ func (form *FormData) append(err error) {
|
||||
form.errors = multierr.Append(form.errors, err)
|
||||
}
|
||||
|
||||
// mustValue binds the target interface with a form data value. If the value is
|
||||
// mustValue binds the target interface with a form field. If the value is
|
||||
// empty or the "key" does not exist, it binds the default value. Currently,
|
||||
// only the string, bool, int, float64 and time.Duration types are bindable.
|
||||
func (form *FormData) mustValue(key string, target interface{}, defaultValue interface{}) *FormData {
|
||||
@@ -343,16 +342,16 @@ func (form *FormData) mustValue(key string, target interface{}, defaultValue int
|
||||
return form.mustAssign(key, val[0], target)
|
||||
}
|
||||
|
||||
// mustMandatoryValue binds the target interface with a form data value. It
|
||||
// mustMandatoryField binds the target interface with a form field. It
|
||||
// populates an error if the value is empty or the "key" does not exist.
|
||||
// Currently, only the string, bool, int, float64 and time.Duration types are
|
||||
// bindable.
|
||||
func (form *FormData) mustMandatoryValue(key string, target interface{}) *FormData {
|
||||
func (form *FormData) mustMandatoryField(key string, target interface{}) *FormData {
|
||||
val, ok := form.values[key]
|
||||
|
||||
if !ok || val[0] == "" {
|
||||
form.append(
|
||||
fmt.Errorf("form value '%s' is required", key),
|
||||
fmt.Errorf("form field '%s' is required", key),
|
||||
)
|
||||
|
||||
return form
|
||||
@@ -386,7 +385,7 @@ func (form *FormData) mustAssign(key, value string, target interface{}) *FormDat
|
||||
|
||||
if err != nil {
|
||||
form.append(
|
||||
fmt.Errorf("form value '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||
fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ type Chromium struct {
|
||||
logger *zap.Logger
|
||||
browser browser
|
||||
supervisor gotenberg.ProcessSupervisor
|
||||
engine gotenberg.PDFEngine
|
||||
engine gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
// Options are the available expectedOptions for converting HTML document to PDF.
|
||||
@@ -293,11 +293,11 @@ func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
|
||||
mod.supervisor = gotenberg.NewProcessSupervisor(mod.logger, mod.browser, flags.MustInt64("chromium-restart-after"))
|
||||
|
||||
// PDF Engine.
|
||||
provider, err := ctx.Module(new(gotenberg.PDFEngineProvider))
|
||||
provider, err := ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||
if err != nil {
|
||||
return fmt.Errorf("get PDF engine provider: %w", err)
|
||||
}
|
||||
engine, err := provider.(gotenberg.PDFEngineProvider).PDFEngine()
|
||||
engine, err := provider.(gotenberg.PdfEngineProvider).PdfEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get PDF engine: %w", err)
|
||||
}
|
||||
@@ -402,7 +402,7 @@ func (mod *Chromium) Checks() ([]health.CheckerOption, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("chromium is unhealthy")
|
||||
return errors.New("Chromium is unhealthy")
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestChromium_Provision(t *testing.T) {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
@@ -116,7 +116,7 @@ func TestChromium_Provision(t *testing.T) {
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestChromium_Provision(t *testing.T) {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.LoggerProviderMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
@@ -145,8 +145,8 @@ func TestChromium_Provision(t *testing.T) {
|
||||
mod.LoggerMock = func(mod gotenberg.Module) (*zap.Logger, error) {
|
||||
return zap.NewNop(), nil
|
||||
}
|
||||
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
return new(gotenberg.PDFEngineMock), nil
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return new(gotenberg.PdfEngineMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
|
||||
@@ -129,8 +129,41 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||
return form, options
|
||||
}
|
||||
|
||||
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
||||
// data. Fallback to default value if the considered key is not present.
|
||||
func FormDataChromiumPdfFormats(ctx *api.Context) gotenberg.PdfFormats {
|
||||
var (
|
||||
pdfFormat string
|
||||
pdfa string
|
||||
pdfua bool
|
||||
)
|
||||
|
||||
ctx.FormData().
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false)
|
||||
|
||||
// FIXME: deprecated.
|
||||
// pdfa > pdfFormat.
|
||||
var actualPdfArchive string
|
||||
|
||||
if pdfFormat != "" {
|
||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
||||
actualPdfArchive = pdfFormat
|
||||
}
|
||||
|
||||
if pdfa != "" {
|
||||
actualPdfArchive = pdfa
|
||||
}
|
||||
|
||||
return gotenberg.PdfFormats{
|
||||
PdfA: actualPdfArchive,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
}
|
||||
|
||||
// convertUrlRoute returns an [api.Route] which can convert a URL to PDF.
|
||||
func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/chromium/convert/url",
|
||||
@@ -138,21 +171,17 @@ func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
||||
|
||||
var (
|
||||
url string
|
||||
pdfFormat string
|
||||
)
|
||||
|
||||
var url string
|
||||
err := form.
|
||||
MandatoryString("url", &url).
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||
}
|
||||
@@ -164,7 +193,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
|
||||
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
|
||||
// PDF.
|
||||
func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/chromium/convert/html",
|
||||
@@ -172,23 +201,18 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
||||
|
||||
var (
|
||||
inputPath string
|
||||
pdfFormat string
|
||||
)
|
||||
|
||||
var inputPath string
|
||||
err := form.
|
||||
MandatoryPath("index.html", &inputPath).
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("file://%s", inputPath)
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||
}
|
||||
@@ -200,7 +224,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
|
||||
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
|
||||
// to PDF.
|
||||
func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/chromium/convert/markdown",
|
||||
@@ -208,17 +232,16 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
form, options := FormDataChromiumPdfOptions(ctx)
|
||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
||||
|
||||
var (
|
||||
inputPath string
|
||||
markdownPaths []string
|
||||
pdfFormat string
|
||||
)
|
||||
|
||||
err := form.
|
||||
MandatoryPath("index.html", &inputPath).
|
||||
MandatoryPaths([]string{".md"}, &markdownPaths).
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
@@ -297,7 +320,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
|
||||
url := fmt.Sprintf("file://%s", inputPath)
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormat, options)
|
||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||
}
|
||||
@@ -308,7 +331,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PDFEngine) api.Route {
|
||||
}
|
||||
|
||||
// convertUrl is a stub which is called by the other methods of this file.
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url, pdfFormat string, options Options) error {
|
||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options Options) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||
@@ -385,22 +408,22 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PDFEngine, url,
|
||||
}
|
||||
|
||||
// So far so good, the URL has been converted to PDF.
|
||||
// Now, let's check if the client want to convert this result PDF
|
||||
// to a specific PDF format.
|
||||
|
||||
if pdfFormat != "" {
|
||||
// Now, let's check if the client want to convert the resulting PDF
|
||||
// to specific formats.
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats != zeroValued {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormat, convertInputPath, convertOutputPath)
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("convert PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", pdfFormat),
|
||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -121,6 +121,58 @@ func TestFormDataChromiumPdfOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormDataChromiumPdfFormats(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
expectedPdfFormats gotenberg.PdfFormats
|
||||
}{
|
||||
{
|
||||
scenario: "no custom form fields",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectedPdfFormats: gotenberg.PdfFormats{},
|
||||
},
|
||||
{
|
||||
scenario: "deprecated pdfFormat form field",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
|
||||
},
|
||||
{
|
||||
scenario: "pdfa and pdfua form fields",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
"foo",
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
expectedPdfFormats: gotenberg.PdfFormats{PdfA: "foo", PdfUa: true},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
actual := FormDataChromiumPdfFormats(tc.ctx.Context)
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.expectedPdfFormats) {
|
||||
t.Fatalf("expected %+v but got: %+v", tc.expectedPdfFormats, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertUrlRoute(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
@@ -535,8 +587,8 @@ func TestConvertUrl(t *testing.T) {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
api Api
|
||||
engine gotenberg.PDFEngine
|
||||
pdfFormat string
|
||||
engine gotenberg.PdfEngine
|
||||
pdfFormats gotenberg.PdfFormats
|
||||
options Options
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
@@ -643,15 +695,15 @@ func TestConvertUrl(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPDFFormatNotAvailable",
|
||||
scenario: "ErrPdfFormatNotSupported",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
api: &ApiMock{func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return nil
|
||||
}},
|
||||
engine: &gotenberg.PDFEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPdfFormatNotSupported
|
||||
}},
|
||||
pdfFormat: "foo",
|
||||
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
|
||||
options: DefaultOptions(),
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
@@ -664,10 +716,10 @@ func TestConvertUrl(t *testing.T) {
|
||||
api: &ApiMock{func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return nil
|
||||
}},
|
||||
engine: &gotenberg.PDFEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
}},
|
||||
pdfFormat: "foo",
|
||||
pdfFormats: gotenberg.PdfFormats{PdfA: "foo"},
|
||||
options: DefaultOptions(),
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
@@ -679,10 +731,10 @@ func TestConvertUrl(t *testing.T) {
|
||||
api: &ApiMock{func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
||||
return nil
|
||||
}},
|
||||
engine: &gotenberg.PDFEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
}},
|
||||
pdfFormat: "foo",
|
||||
pdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1a},
|
||||
options: DefaultOptions(),
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
@@ -717,7 +769,7 @@ func TestConvertUrl(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.pdfFormat, tc.options)
|
||||
err := convertUrl(tc.ctx.Context, tc.api, tc.engine, "", tc.pdfFormats, tc.options)
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
|
||||
@@ -47,17 +47,17 @@ type Options struct {
|
||||
Landscape bool
|
||||
|
||||
// PageRanges allows to select the pages to convert.
|
||||
// TODO: should prefer a method form PDFEngine.
|
||||
// TODO: should prefer a method form PdfEngine.
|
||||
// Optional.
|
||||
PageRanges string
|
||||
|
||||
// PdfFormat allows to convert the resulting PDF to PDF/A-1a, PDF/A-2b, or
|
||||
// PDF/A-3b.
|
||||
// PdfFormats allows to convert the resulting PDF to PDF/A-1a, PDF/A-2b,
|
||||
// PDF/A-3b and PDF/UA.
|
||||
// Optional.
|
||||
PdfFormat string
|
||||
PdfFormats gotenberg.PdfFormats
|
||||
}
|
||||
|
||||
// Uno is an abstraction on top of Api.
|
||||
// Uno is an abstraction on top of the Universal Network Objects API.
|
||||
type Uno interface {
|
||||
Pdf(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options Options) error
|
||||
Extensions() []string
|
||||
@@ -265,13 +265,13 @@ func (a *Api) Metrics() ([]gotenberg.Metric, error) {
|
||||
func (a *Api) Checks() ([]health.CheckerOption, error) {
|
||||
return []health.CheckerOption{
|
||||
health.WithCheck(health.Check{
|
||||
Name: "api",
|
||||
Name: "libreoffice",
|
||||
Check: func(_ context.Context) error {
|
||||
if a.supervisor.Healthy() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("LibreOffice unhealthy")
|
||||
return errors.New("LibreOffice is unhealthy")
|
||||
},
|
||||
}),
|
||||
}, nil
|
||||
|
||||
@@ -270,18 +270,26 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
args = append(args, "--export", fmt.Sprintf("PageRange=%s", options.PageRanges))
|
||||
}
|
||||
|
||||
switch options.PdfFormat {
|
||||
switch options.PdfFormats.PdfA {
|
||||
case "":
|
||||
case gotenberg.FormatPDFA1a:
|
||||
case gotenberg.PdfA1a:
|
||||
args = append(args, "--export", "SelectPdfVersion=1")
|
||||
case gotenberg.FormatPDFA2b:
|
||||
case gotenberg.PdfA2b:
|
||||
args = append(args, "--export", "SelectPdfVersion=2")
|
||||
case gotenberg.FormatPDFA3b:
|
||||
case gotenberg.PdfA3b:
|
||||
args = append(args, "--export", "SelectPdfVersion=3")
|
||||
default:
|
||||
return ErrInvalidPdfFormat
|
||||
}
|
||||
|
||||
if options.PdfFormats.PdfUa {
|
||||
args = append(
|
||||
args,
|
||||
"--export", "EnableTextAccessForAccessibilityTools=true",
|
||||
"--export", "UseTaggedPDF=true",
|
||||
)
|
||||
}
|
||||
|
||||
args = append(args, "--output", outputPath, inputPath)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, p.arguments.unoBinPath, args...)
|
||||
|
||||
@@ -243,7 +243,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
return p
|
||||
}(),
|
||||
fs: gotenberg.NewFileSystem(),
|
||||
options: Options{PdfFormat: "foo"},
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: "foo"}},
|
||||
cancelledCtx: false,
|
||||
start: false,
|
||||
expectError: true,
|
||||
@@ -417,7 +417,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormat: gotenberg.FormatPDFA1a},
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA1a}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
@@ -446,7 +446,7 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormat: gotenberg.FormatPDFA2b},
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA2b}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
@@ -475,7 +475,36 @@ func TestLibreOfficeProcess_pdf(t *testing.T) {
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormat: gotenberg.FormatPDFA3b},
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfA: gotenberg.PdfA3b}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "success (PDF/UA)",
|
||||
libreOffice: newLibreOfficeProcess(
|
||||
libreOfficeArguments{
|
||||
binPath: os.Getenv("LIBREOFFICE_BIN_PATH"),
|
||||
unoBinPath: os.Getenv("UNOCONV_BIN_PATH"),
|
||||
startTimeout: 5 * time.Second,
|
||||
},
|
||||
),
|
||||
fs: func() *gotenberg.FileSystem {
|
||||
fs := gotenberg.NewFileSystem()
|
||||
|
||||
err := os.MkdirAll(fs.WorkingDirPath(), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf(fmt.Sprintf("expected no error but got: %v", err))
|
||||
}
|
||||
|
||||
err = os.WriteFile(fmt.Sprintf("%s/document.txt", fs.WorkingDirPath()), []byte("Landscape"), 0o755)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return fs
|
||||
}(),
|
||||
options: Options{PdfFormats: gotenberg.PdfFormats{PdfUa: true}},
|
||||
cancelledCtx: false,
|
||||
start: true,
|
||||
expectError: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ func init() {
|
||||
// PDF with LibreOffice.
|
||||
type LibreOffice struct {
|
||||
api libeofficeapi.Uno
|
||||
engine gotenberg.PDFEngine
|
||||
engine gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
|
||||
@@ -53,12 +53,12 @@ func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
|
||||
|
||||
mod.api = libreOfficeApi
|
||||
|
||||
provider, err = ctx.Module(new(gotenberg.PDFEngineProvider))
|
||||
provider, err = ctx.Module(new(gotenberg.PdfEngineProvider))
|
||||
if err != nil {
|
||||
return fmt.Errorf("get PDF engine provider: %w", err)
|
||||
}
|
||||
|
||||
engine, err := provider.(gotenberg.PDFEngineProvider).PDFEngine()
|
||||
engine, err := provider.(gotenberg.PdfEngineProvider).PdfEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get PDF engine: %w", err)
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
libreofficeapi.ProviderMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
@@ -102,7 +102,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||
return new(libreofficeapi.ApiMock), nil
|
||||
}
|
||||
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return nil, errors.New("foo")
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
mod := &struct {
|
||||
gotenberg.ModuleMock
|
||||
libreofficeapi.ProviderMock
|
||||
gotenberg.PDFEngineProviderMock
|
||||
gotenberg.PdfEngineProviderMock
|
||||
}{}
|
||||
mod.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return mod }}
|
||||
@@ -131,8 +131,8 @@ func TestLibreOffice_Provision(t *testing.T) {
|
||||
mod.LibreOfficeMock = func() (libreofficeapi.Uno, error) {
|
||||
return new(libreofficeapi.ApiMock), nil
|
||||
}
|
||||
mod.PDFEngineMock = func() (gotenberg.PDFEngine, error) {
|
||||
return new(gotenberg.PDFEngineMock), nil
|
||||
mod.PdfEngineMock = func() (gotenberg.PdfEngine, error) {
|
||||
return new(gotenberg.PdfEngineMock), nil
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Package pdfengine provides a module which interacts with LibreOffice via the
|
||||
// UNO (Universal Network Objects) API and implements the [gotenberg.PDFEngine]
|
||||
// UNO (Universal Network Objects) API and implements the gotenberg.PdfEngine
|
||||
// interface.
|
||||
package pdfengine
|
||||
|
||||
@@ -16,7 +16,7 @@ func init() {
|
||||
}
|
||||
|
||||
// LibreOfficePdfEngine interacts with the LibreOffice (Universal Network Objects) API
|
||||
// and implements the [gotenberg.PDFEngine] interface.
|
||||
// and implements the [gotenberg.PdfEngine] interface.
|
||||
type LibreOfficePdfEngine struct {
|
||||
unoAPI api.Uno
|
||||
}
|
||||
@@ -46,17 +46,18 @@ func (engine *LibreOfficePdfEngine) Provision(ctx *gotenberg.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge is not available for this PDF engine.
|
||||
func (engine *LibreOfficePdfEngine) Merge(_ context.Context, _ *zap.Logger, _ []string, _ string) error {
|
||||
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||
// Merge is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return fmt.Errorf("merge PDFs with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||
// PDF/A-1a, PDF/A-2b and PDF/A-3b formats are available. If another PDF format
|
||||
// is requested, it returns a [gotenberg.ErrPDFFormatNotAvailable] error.
|
||||
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
// PDF/A-1a, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
||||
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
||||
// error.
|
||||
func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
err := engine.unoAPI.Pdf(ctx, logger, inputPath, outputPath, api.Options{
|
||||
PdfFormat: format,
|
||||
PdfFormats: formats,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
@@ -64,15 +65,15 @@ func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logger *zap.Log
|
||||
}
|
||||
|
||||
if errors.Is(err, api.ErrInvalidPdfFormat) {
|
||||
return fmt.Errorf("convert PDF to '%s' with LibreOffice: %w", format, gotenberg.ErrPDFFormatNotAvailable)
|
||||
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, gotenberg.ErrPdfFormatNotSupported)
|
||||
}
|
||||
|
||||
return fmt.Errorf("convert PDF to '%s' with unoconv: %w", format, err)
|
||||
return fmt.Errorf("convert PDF to '%+v' with LibreOffice: %w", formats, err)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
||||
_ gotenberg.Provisioner = (*LibreOfficePdfEngine)(nil)
|
||||
_ gotenberg.PDFEngine = (*LibreOfficePdfEngine)(nil)
|
||||
_ gotenberg.PdfEngine = (*LibreOfficePdfEngine)(nil)
|
||||
)
|
||||
|
||||
@@ -95,8 +95,8 @@ func TestLibreOfficePdfEngine_Provider(t *testing.T) {
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(LibreOfficePdfEngine)
|
||||
err := mod.Provision(tc.ctx)
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.Provision(tc.ctx)
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
@@ -110,11 +110,11 @@ func TestLibreOfficePdfEngine_Provider(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLibreOfficePdfEngine_Merge(t *testing.T) {
|
||||
mod := new(LibreOfficePdfEngine)
|
||||
err := mod.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
engine := new(LibreOfficePdfEngine)
|
||||
err := engine.Merge(context.Background(), zap.NewNop(), nil, "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := &LibreOfficePdfEngine{unoAPI: tc.api}
|
||||
err := engine.Convert(context.Background(), zap.NewNop(), "", "", "")
|
||||
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
||||
// to PDF.
|
||||
func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) api.Route {
|
||||
func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/libreoffice/convert",
|
||||
@@ -30,6 +30,9 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
|
||||
nativePdfA1aFormat bool
|
||||
nativePdfFormat string
|
||||
pdfFormat string
|
||||
pdfa string
|
||||
pdfua bool
|
||||
nativePdfFormats bool
|
||||
merge bool
|
||||
)
|
||||
|
||||
@@ -40,50 +43,69 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
|
||||
Bool("nativePdfA1aFormat", &nativePdfA1aFormat, false).
|
||||
String("nativePdfFormat", &nativePdfFormat, "").
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false).
|
||||
Bool("nativePdfFormats", &nativePdfFormats, true).
|
||||
Bool("merge", &merge, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
// nativePdfFormat > pdfFormat > nativePdfA1aFormat.
|
||||
// FIXME: deprecated.
|
||||
// pdfa > nativePdfFormat > pdfFormat > nativePdfA1aFormat.
|
||||
var (
|
||||
actualPdfFormat string
|
||||
nativeFormat bool
|
||||
actualPdfArchive string
|
||||
nativeFormats bool
|
||||
)
|
||||
|
||||
// FIXME: deprecated.
|
||||
if nativePdfA1aFormat {
|
||||
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer 'nativePdfFormat' or 'pdfFormat' form fields instead")
|
||||
actualPdfFormat = gotenberg.FormatPDFA1a
|
||||
nativeFormat = true
|
||||
ctx.Log().Warn("'nativePdfA1aFormat' is deprecated; prefer the 'pdfa' form field instead")
|
||||
actualPdfArchive = gotenberg.PdfA1a
|
||||
nativeFormats = true
|
||||
}
|
||||
|
||||
if pdfFormat != "" {
|
||||
actualPdfFormat = pdfFormat
|
||||
nativeFormat = false
|
||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
||||
actualPdfArchive = pdfFormat
|
||||
nativeFormats = false
|
||||
}
|
||||
|
||||
if nativePdfFormat != "" {
|
||||
actualPdfFormat = nativePdfFormat
|
||||
nativeFormat = true
|
||||
ctx.Log().Warn("'nativePdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
||||
actualPdfArchive = nativePdfFormat
|
||||
nativeFormats = true
|
||||
}
|
||||
|
||||
if pdfa != "" {
|
||||
actualPdfArchive = pdfa
|
||||
nativeFormats = nativePdfFormats
|
||||
}
|
||||
|
||||
if pdfua {
|
||||
nativeFormats = nativePdfFormats
|
||||
}
|
||||
|
||||
pdfFormats := gotenberg.PdfFormats{
|
||||
PdfA: actualPdfArchive,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
|
||||
// Alright, let's convert each document to PDF.
|
||||
|
||||
outputPaths := make([]string, len(inputPaths))
|
||||
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
|
||||
options := libreofficeapi.Options{
|
||||
Landscape: landscape,
|
||||
PageRanges: nativePageRanges,
|
||||
PdfFormat: nativePdfFormat,
|
||||
}
|
||||
|
||||
if nativeFormats {
|
||||
options.PdfFormats = pdfFormats
|
||||
}
|
||||
|
||||
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, libreofficeapi.ErrMalformedPageRanges) {
|
||||
return api.WrapError(
|
||||
@@ -108,21 +130,20 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
|
||||
}
|
||||
|
||||
// Now, let's check if the client want to convert this result
|
||||
// PDF to a specific PDF format.
|
||||
|
||||
if !nativeFormat && actualPdfFormat != "" {
|
||||
// PDF to specific PDF formats.
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if !nativeFormats && pdfFormats != zeroValued {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), actualPdfFormat, convertInputPath, convertOutputPath)
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("convert PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", actualPdfFormat),
|
||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -147,23 +168,22 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PDFEngine) ap
|
||||
|
||||
// Ok, we don't have to merge the PDFs. Let's check if the client
|
||||
// want to convert each PDF to a specific PDF format.
|
||||
|
||||
if !nativeFormat && actualPdfFormat != "" {
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if !nativeFormats && pdfFormats != zeroValued {
|
||||
convertOutputPaths := make([]string, len(outputPaths))
|
||||
|
||||
for i, outputPath := range outputPaths {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), actualPdfFormat, convertInputPath, convertOutputPaths[i])
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPaths[i])
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("convert PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", actualPdfFormat),
|
||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
libreOffice libreofficeapi.Uno
|
||||
engine gotenberg.PDFEngine
|
||||
engine gotenberg.PdfEngine
|
||||
expectOptions libreofficeapi.Options
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
@@ -81,7 +81,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPDFFormatNotAvailable (single file)",
|
||||
scenario: "ErrPdfFormatNotSupported (single file)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -102,9 +102,9 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPdfFormatNotSupported
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
@@ -121,7 +121,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
@@ -134,8 +134,8 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
@@ -209,15 +209,18 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectOutputPathsCount: 2,
|
||||
},
|
||||
{
|
||||
scenario: "success with PDF format (single file)",
|
||||
scenario: "success with non-native PDF/A (single file)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"document.docx": "/document.docx",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
@@ -230,8 +233,8 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -240,7 +243,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success with every PDF formats form field (single file)",
|
||||
scenario: "success with every non-native PDF/A & PDF/UA form fields (single file)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -251,10 +254,19 @@ func TestConvertRoute(t *testing.T) {
|
||||
"true",
|
||||
},
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"nativePdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
@@ -267,8 +279,8 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -299,7 +311,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
@@ -309,7 +321,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPDFFormatNotAvailable (merge)",
|
||||
scenario: "ErrPdfFormatNotSupported (merge)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -320,9 +332,12 @@ func TestConvertRoute(t *testing.T) {
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
"pdfFormat": {
|
||||
"pdfa": {
|
||||
"foo",
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
@@ -334,12 +349,12 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPdfFormatNotSupported
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
@@ -359,8 +374,11 @@ func TestConvertRoute(t *testing.T) {
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
@@ -373,11 +391,11 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
@@ -409,7 +427,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -441,7 +459,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -451,7 +469,7 @@ func TestConvertRoute(t *testing.T) {
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success with PDF format (merge)",
|
||||
scenario: "success with non-native PDF/A (merge)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
@@ -462,8 +480,11 @@ func TestConvertRoute(t *testing.T) {
|
||||
"merge": {
|
||||
"true",
|
||||
},
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"nativePdfFormats": {
|
||||
"false",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
@@ -476,11 +497,11 @@ func TestConvertRoute(t *testing.T) {
|
||||
return []string{".docx"}
|
||||
},
|
||||
},
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Package logging provides a module which creates a [zap.Logger] for other
|
||||
// modules.
|
||||
// Package logging provides a module which creates a zap.Logger instance for
|
||||
// other modules.
|
||||
package logging
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
func TestLogging_Descriptor(t *testing.T) {
|
||||
descriptor := new(Logging).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(Logging))
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Package pdfcpu provides a module which wraps the
|
||||
// https://github.com/pdfcpu/pdfcpu library and implements the
|
||||
// gotenberg.PDFEngine interface.
|
||||
// Package pdfcpu provides an implementation of the gotenberg.PdfEngine
|
||||
// interface using the pdfcpu library. This package allows for the merging of
|
||||
// PDF files but does not support conversion to specific PDF formats.
|
||||
//
|
||||
// See: https://github.com/pdfcpu/pdfcpu.
|
||||
package pdfcpu
|
||||
|
||||
@@ -13,35 +13,34 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
gotenberg.MustRegisterModule(PDFcpu{})
|
||||
gotenberg.MustRegisterModule(new(PdfCpu))
|
||||
}
|
||||
|
||||
// PDFcpu is a module which wraps the https://github.com/pdfcpu/pdfcpu library
|
||||
// and implements the gotenberg.PDFEngine interface.
|
||||
type PDFcpu struct {
|
||||
// PdfCpu abstracts the pdfcpu library and implements the [gotenberg.PdfEngine]
|
||||
// interface.
|
||||
type PdfCpu struct {
|
||||
conf *pdfcpuConfig.Configuration
|
||||
}
|
||||
|
||||
// Descriptor returns a PDFcpu's module descriptor.
|
||||
func (PDFcpu) Descriptor() gotenberg.ModuleDescriptor {
|
||||
// Descriptor returns a [PdfCpu]'s module descriptor.
|
||||
func (engine *PdfCpu) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
ID: "pdfcpu",
|
||||
New: func() gotenberg.Module { return new(PDFcpu) },
|
||||
New: func() gotenberg.Module { return new(PdfCpu) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets the engine properties.
|
||||
func (engine *PDFcpu) Provision(_ *gotenberg.Context) error {
|
||||
func (engine *PdfCpu) Provision(ctx *gotenberg.Context) error {
|
||||
pdfcpuConfig.ConfigPath = "disable"
|
||||
pdfcpuLog.DisableLoggers()
|
||||
|
||||
engine.conf = pdfcpuConfig.NewDefaultConfiguration()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge merges the given PDFs into a unique Pdf.
|
||||
func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
err := pdfcpuAPI.MergeCreateFile(inputPaths, outputPath, engine.conf)
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -50,14 +49,14 @@ func (engine PDFcpu) Merge(_ context.Context, _ *zap.Logger, inputPaths []string
|
||||
return fmt.Errorf("merge PDFs with PDFcpu: %w", err)
|
||||
}
|
||||
|
||||
// Convert is not available for this Pdf engine.
|
||||
func (engine PDFcpu) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
|
||||
return fmt.Errorf("convert Pdf to '%s' with PDFcpu: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with PDFcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*PDFcpu)(nil)
|
||||
_ gotenberg.Provisioner = (*PDFcpu)(nil)
|
||||
_ gotenberg.PDFEngine = (*PDFcpu)(nil)
|
||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfCpu)(nil)
|
||||
_ gotenberg.PdfEngine = (*PdfCpu)(nil)
|
||||
)
|
||||
|
||||
@@ -12,89 +12,93 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestPDFcpu_Descriptor(t *testing.T) {
|
||||
descriptor := PDFcpu{}.Descriptor()
|
||||
func TestPdfCpu_Descriptor(t *testing.T) {
|
||||
descriptor := new(PdfCpu).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(PDFcpu))
|
||||
expect := reflect.TypeOf(new(PdfCpu))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFcpu_Provision(t *testing.T) {
|
||||
mod := new(PDFcpu)
|
||||
func TestPdfCpu_Provision(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||
|
||||
err := mod.Provision(ctx)
|
||||
err := engine.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFcpu_Merge(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
func TestPdfCpu_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
inputPaths []string
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "single file success",
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
mod := new(PDFcpu)
|
||||
|
||||
err := mod.Provision(nil)
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfCpu)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem()
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(fs.WorkingDirPath())
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = mod.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
|
||||
err = engine.Merge(nil, nil, tc.inputPaths, outputDir+"/foo.pdf")
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFcpu_Convert(t *testing.T) {
|
||||
mod := new(PDFcpu)
|
||||
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
|
||||
func TestPdfCpu_Convert(t *testing.T) {
|
||||
mod := new(PdfCpu)
|
||||
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// Package pdfengines provides a module which gathers modules that implements
|
||||
// the gotenberg.PDFEngine interface.
|
||||
// Package pdfengines a way to gather and manage multiple modules that
|
||||
// implement the gotenberg.PdfEngine interface.
|
||||
package pdfengines
|
||||
|
||||
@@ -10,30 +10,24 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// multiPDFEngines implements the gotenberg.PDFEngine interface and gathers one
|
||||
// or more gotenberg.PDFEngine. It provides a sort of fallback mechanism: if an
|
||||
// engine's method returns an error, it calls the same method from another
|
||||
// engine.
|
||||
type multiPDFEngines struct {
|
||||
engines []gotenberg.PDFEngine
|
||||
type multiPdfEngines struct {
|
||||
engines []gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
// newMultiPDFEngines returns a multiPDFEngines. Arguments' order determines the
|
||||
// order of the engines called.
|
||||
func newMultiPDFEngines(engines ...gotenberg.PDFEngine) *multiPDFEngines {
|
||||
return &multiPDFEngines{
|
||||
func newMultiPdfEngines(engines ...gotenberg.PdfEngine) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
engines: engines,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge tries to merge the given PDFs into a unique PDF thanks to its
|
||||
// children. If the context is done, it stops and returns an error.
|
||||
func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.engines {
|
||||
go func(engine gotenberg.PDFEngine) {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
|
||||
}(engine)
|
||||
|
||||
@@ -53,13 +47,13 @@ func (multi multiPDFEngines) Merge(ctx context.Context, logger *zap.Logger, inpu
|
||||
|
||||
// Convert converts the given PDF to a specific PDF format. thanks to its
|
||||
// children. If the context is done, it stops and returns an error.
|
||||
func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.engines {
|
||||
go func(engine gotenberg.PDFEngine) {
|
||||
errChan <- engine.Convert(ctx, logger, format, inputPath, outputPath)
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
@@ -73,10 +67,10 @@ func (multi multiPDFEngines) Convert(ctx context.Context, logger *zap.Logger, fo
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("convert PDF to '%s' with multi PDF engines: %w", format, err)
|
||||
return fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.PDFEngine = (*multiPDFEngines)(nil)
|
||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||
)
|
||||
|
||||
@@ -10,61 +10,63 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
engine *multiPDFEngines
|
||||
func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectMergeErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "nominal behavior",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "at least one engine does not return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PDFEngineMock{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "all engines return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PDFEngineMock{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectMergeErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "context expired",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
@@ -76,37 +78,35 @@ func TestMultiPDFEngines_Merge(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectMergeErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
|
||||
|
||||
if tc.expectMergeErr && err == nil {
|
||||
t.Errorf("expected engine.Merge() error, but got none")
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectMergeErr && err != nil {
|
||||
t.Errorf("expected no error from engine.Merge(), but got: %v", err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
engine *multiPDFEngines
|
||||
func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectConvertErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "nominal behavior",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
scenario: "nominal behavior",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -114,15 +114,15 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
name: "at least one engine does not return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -130,27 +130,27 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
name: "all engines return an error",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
scenario: "all engines return an error",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
),
|
||||
ctx: context.Background(),
|
||||
expectConvertErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "context expired",
|
||||
engine: newMultiPDFEngines(
|
||||
&gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
scenario: "context expired",
|
||||
engine: newMultiPdfEngines(
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
@@ -161,20 +161,18 @@ func TestMultiPDFEngines_Convert(t *testing.T) {
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectConvertErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Convert(tc.ctx, zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.engine.Convert(tc.ctx, zap.NewNop(), "", "", "")
|
||||
|
||||
if tc.expectConvertErr && err == nil {
|
||||
t.Errorf("expected engine.Convert() error, but got none")
|
||||
}
|
||||
|
||||
if !tc.expectConvertErr && err != nil {
|
||||
t.Errorf("expected no error from engine.Convert(), but got: %v", err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,28 +12,28 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
gotenberg.MustRegisterModule(PDFEngines{})
|
||||
gotenberg.MustRegisterModule(new(PdfEngines))
|
||||
}
|
||||
|
||||
// PDFEngines is a module which gathers available gotenberg.PDFEngine modules.
|
||||
// The available gotenberg.PDFEngine modules can be either all
|
||||
// gotenberg.PDFEngine modules or the modules selected by the user thanks to
|
||||
// the "engines" flag.
|
||||
// PdfEngines acts as an aggregator and manager for multiple PDF engine
|
||||
// modules. It enables the selection and ordering of PDF engines based on user
|
||||
// preferences passed via command-line flags. The [PdfEngines] module also
|
||||
// implements the [gotenberg.PdfEngine] interface, providing a unified approach
|
||||
// to PDF processing across the various engines it manages.
|
||||
//
|
||||
// PDFEngines wraps the gotenberg.PDFEngine modules in an internal struct which
|
||||
// also implements gotenberg.PDFEngine. This struct provides a sort of fallback
|
||||
// mechanism: if an engine's method returns an error, it calls the same method
|
||||
// from another engine.
|
||||
//
|
||||
// This module implements the gotenberg.PDFEngineProvider interface.
|
||||
type PDFEngines struct {
|
||||
// When processing PDFs, [PdfEngines] will attempt to use the engines in the
|
||||
// order they were defined. If the primary engine encounters an error,
|
||||
// [PdfEngines] can fall back to the next available engine. It also implements
|
||||
// the [api.Router] interface to expose relevant PDF processing routes if
|
||||
// enabled.
|
||||
type PdfEngines struct {
|
||||
names []string
|
||||
engines []gotenberg.PDFEngine
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
|
||||
// Descriptor returns a PDFEngines' module descriptor.
|
||||
func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
// Descriptor returns a PdfEngines' module descriptor.
|
||||
func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
ID: "pdfengines",
|
||||
FlagSet: func() *flag.FlagSet {
|
||||
@@ -43,13 +43,13 @@ func (PDFEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
|
||||
return fs
|
||||
}(),
|
||||
New: func() gotenberg.Module { return new(PDFEngines) },
|
||||
New: func() gotenberg.Module { return new(PdfEngines) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision gets either all gotenberg.PDFEngine modules or the modules
|
||||
// Provision gets either all [gotenberg.PdfEngine] modules or the modules
|
||||
// selected by the user thanks to the "engines" flag.
|
||||
func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
||||
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
flags := ctx.ParsedFlags()
|
||||
names := flags.MustStringSlice("pdfengines-engines")
|
||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||
@@ -66,15 +66,15 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
||||
|
||||
logger = logger.Named("pdfengines")
|
||||
|
||||
engines, err := ctx.Modules(new(gotenberg.PDFEngine))
|
||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||
if err != nil {
|
||||
return fmt.Errorf("get PDF engines: %w", err)
|
||||
}
|
||||
|
||||
mod.engines = make([]gotenberg.PDFEngine, len(engines))
|
||||
mod.engines = make([]gotenberg.PdfEngine, len(engines))
|
||||
|
||||
for i, engine := range engines {
|
||||
mod.engines[i] = engine.(gotenberg.PDFEngine)
|
||||
mod.engines[i] = engine.(gotenberg.PdfEngine)
|
||||
}
|
||||
|
||||
if len(names) > 0 {
|
||||
@@ -82,9 +82,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.names = names
|
||||
|
||||
for i, name := range names {
|
||||
logger.Warn("unoconv-pdfengine is deprecated; prefer api-pdfengine instead")
|
||||
if name == "unoconv-pdfengine" {
|
||||
mod.names[i] = "api-pdfengine"
|
||||
// FIXME: deprecated.
|
||||
if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
||||
logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
||||
mod.names[i] = "libreoffice-pdfengine"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +102,10 @@ func (mod *PDFEngines) Provision(ctx *gotenberg.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates there is at least one gotenberg.PDFEngine module
|
||||
// available. It also validates that selected gotenberg.PDFEngine modules
|
||||
// Validate validates there is at least one [gotenberg.PdfEngine] module
|
||||
// available. It also validates that selected [gotenberg.PdfEngine] modules
|
||||
// actually exist.
|
||||
func (mod PDFEngines) Validate() error {
|
||||
func (mod *PdfEngines) Validate() error {
|
||||
if len(mod.engines) == 0 {
|
||||
return errors.New("no PDF engine")
|
||||
}
|
||||
@@ -139,17 +140,17 @@ func (mod PDFEngines) Validate() error {
|
||||
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
|
||||
}
|
||||
|
||||
// SystemMessages returns one message with the selected gotenberg.PDFEngine
|
||||
// SystemMessages returns one message with the selected [gotenberg.PdfEngine]
|
||||
// modules.
|
||||
func (mod PDFEngines) SystemMessages() []string {
|
||||
func (mod *PdfEngines) SystemMessages() []string {
|
||||
return []string{
|
||||
strings.Join(mod.names[:], " "),
|
||||
}
|
||||
}
|
||||
|
||||
// PDFEngine returns a gotenberg.PDFEngine.
|
||||
func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
|
||||
engines := make([]gotenberg.PDFEngine, len(mod.names))
|
||||
// PdfEngine returns a [gotenberg.PdfEngine].
|
||||
func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
engines := make([]gotenberg.PdfEngine, len(mod.names))
|
||||
|
||||
for i, name := range mod.names {
|
||||
for _, engine := range mod.engines {
|
||||
@@ -160,20 +161,20 @@ func (mod PDFEngines) PDFEngine() (gotenberg.PDFEngine, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return newMultiPDFEngines(engines...), nil
|
||||
return newMultiPdfEngines(engines...), nil
|
||||
}
|
||||
|
||||
// Routes returns the HTTP routes.
|
||||
func (mod PDFEngines) Routes() ([]api.Route, error) {
|
||||
func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
if mod.disableRoutes {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
engine, err := mod.PDFEngine()
|
||||
engine, err := mod.PdfEngine()
|
||||
if err != nil {
|
||||
// Should not happen, unless our provider implementation
|
||||
// changes in the future.
|
||||
return nil, fmt.Errorf("get pdf engine: %w", err)
|
||||
return nil, fmt.Errorf("get pdf mod: %w", err)
|
||||
}
|
||||
|
||||
return []api.Route{
|
||||
@@ -184,10 +185,10 @@ func (mod PDFEngines) Routes() ([]api.Route, error) {
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*PDFEngines)(nil)
|
||||
_ gotenberg.Provisioner = (*PDFEngines)(nil)
|
||||
_ gotenberg.Validator = (*PDFEngines)(nil)
|
||||
_ gotenberg.SystemLogger = (*PDFEngines)(nil)
|
||||
_ gotenberg.PDFEngineProvider = (*PDFEngines)(nil)
|
||||
_ api.Router = (*PDFEngines)(nil)
|
||||
_ gotenberg.Module = (*PdfEngines)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfEngines)(nil)
|
||||
_ gotenberg.Validator = (*PdfEngines)(nil)
|
||||
_ gotenberg.SystemLogger = (*PdfEngines)(nil)
|
||||
_ gotenberg.PdfEngineProvider = (*PdfEngines)(nil)
|
||||
_ api.Router = (*PdfEngines)(nil)
|
||||
)
|
||||
|
||||
@@ -11,26 +11,26 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestPDFEngines_Descriptor(t *testing.T) {
|
||||
descriptor := PDFEngines{}.Descriptor()
|
||||
func TestPdfEngines_Descriptor(t *testing.T) {
|
||||
descriptor := new(PdfEngines).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(PDFEngines))
|
||||
expect := reflect.TypeOf(new(PdfEngines))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngines_Provision(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
func TestPdfEngines_Provision(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *gotenberg.Context
|
||||
expectPDFEngineNames []string
|
||||
expectProvisionErr bool
|
||||
expectedPdfEngines []string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "no selection from user",
|
||||
scenario: "no selection from user",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
@@ -48,7 +48,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||
@@ -59,7 +59,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
@@ -67,10 +67,11 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectPDFEngineNames: []string{"bar"},
|
||||
expectedPdfEngines: []string{"bar"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "selection from user",
|
||||
scenario: "selection from user",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
@@ -88,7 +89,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "a", New: func() gotenberg.Module { return engine1 }}
|
||||
@@ -100,7 +101,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "b", New: func() gotenberg.Module { return engine2 }}
|
||||
@@ -109,10 +110,10 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fs := new(PDFEngines).Descriptor().FlagSet
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-engines=b", "--pdfengines-engines=a"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
@@ -126,10 +127,11 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectPDFEngineNames: []string{"b", "a"},
|
||||
expectedPdfEngines: []string{"b", "a"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "user select deprecated unoconv-pdfengine",
|
||||
scenario: "user select deprecated unoconv-pdfengine",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
@@ -147,19 +149,19 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "api-pdfengine", New: func() gotenberg.Module { return engine }}
|
||||
return gotenberg.ModuleDescriptor{ID: "libreoffice-pdfengine", New: func() gotenberg.Module { return engine }}
|
||||
}
|
||||
engine.ValidateMock = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fs := new(PDFEngines).Descriptor().FlagSet
|
||||
fs := new(PdfEngines).Descriptor().FlagSet
|
||||
err := fs.Parse([]string{"--pdfengines-engines=unoconv-pdfengine"})
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error from fs.Parse(), but got: %v", err)
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
return gotenberg.NewContext(
|
||||
@@ -172,22 +174,23 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectPDFEngineNames: []string{"api-pdfengine"},
|
||||
expectedPdfEngines: []string{"libreoffice-pdfengine"},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "no logger provider",
|
||||
scenario: "no logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{},
|
||||
)
|
||||
}(),
|
||||
expectProvisionErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "no logger from logger provider",
|
||||
scenario: "no logger from logger provider",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
@@ -204,17 +207,17 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectProvisionErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "no valid PDF engines",
|
||||
scenario: "no valid PDF engine",
|
||||
ctx: func() *gotenberg.Context {
|
||||
provider := &struct {
|
||||
gotenberg.ModuleMock
|
||||
@@ -232,7 +235,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.ValidatorMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine }}
|
||||
@@ -243,7 +246,7 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
|
||||
return gotenberg.NewContext(
|
||||
gotenberg.ParsedFlags{
|
||||
FlagSet: new(PDFEngines).Descriptor().FlagSet,
|
||||
FlagSet: new(PdfEngines).Descriptor().FlagSet,
|
||||
},
|
||||
[]gotenberg.ModuleDescriptor{
|
||||
provider.Descriptor(),
|
||||
@@ -251,67 +254,66 @@ func TestPDFEngines_Provision(t *testing.T) {
|
||||
},
|
||||
)
|
||||
}(),
|
||||
expectProvisionErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mod := new(PDFEngines)
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
err := mod.Provision(tc.ctx)
|
||||
|
||||
if tc.expectProvisionErr && err == nil {
|
||||
t.Fatal("expected mod.Provision() error, but got none")
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectProvisionErr && err != nil {
|
||||
t.Fatalf("expected no error from mod.Provision(), but got: %v", err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
|
||||
if len(tc.expectPDFEngineNames) != len(mod.names) {
|
||||
t.Errorf("expected %d names but got %d", len(tc.expectPDFEngineNames), len(mod.names))
|
||||
if len(tc.expectedPdfEngines) != len(mod.names) {
|
||||
t.Fatalf("expected %d names but got %d", len(tc.expectedPdfEngines), len(mod.names))
|
||||
}
|
||||
|
||||
for index, name := range mod.names {
|
||||
if name != tc.expectPDFEngineNames[index] {
|
||||
t.Errorf("expected name at index %d to be %s, but got: %s", index, name, tc.expectPDFEngineNames[index])
|
||||
if name != tc.expectedPdfEngines[index] {
|
||||
t.Fatalf("expected scenario at index %d to be %s, but got: %s", index, name, tc.expectedPdfEngines[index])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngines_Validate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
func TestPdfEngines_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
names []string
|
||||
engines []gotenberg.PDFEngine
|
||||
expectValidateErr bool
|
||||
engines []gotenberg.PdfEngine
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "existing PDF engine",
|
||||
scenario: "existing PDF engine",
|
||||
names: []string{"foo"},
|
||||
engines: func() []gotenberg.PDFEngine {
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine }}
|
||||
}
|
||||
|
||||
return []gotenberg.PDFEngine{
|
||||
return []gotenberg.PdfEngine{
|
||||
engine,
|
||||
}
|
||||
}(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-existing bar PDF engine",
|
||||
scenario: "non-existing bar PDF engine",
|
||||
names: []string{"foo", "bar", "baz"},
|
||||
engines: func() []gotenberg.PDFEngine {
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||
@@ -319,67 +321,65 @@ func TestPDFEngines_Validate(t *testing.T) {
|
||||
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "baz", New: func() gotenberg.Module { return engine2 }}
|
||||
}
|
||||
|
||||
return []gotenberg.PDFEngine{
|
||||
return []gotenberg.PdfEngine{
|
||||
engine1,
|
||||
engine2,
|
||||
}
|
||||
}(),
|
||||
expectValidateErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "no PDF engine",
|
||||
expectValidateErr: true,
|
||||
scenario: "no PDF engine",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mod := PDFEngines{
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
names: tc.names,
|
||||
engines: tc.engines,
|
||||
}
|
||||
|
||||
err := mod.Validate()
|
||||
|
||||
if tc.expectValidateErr && err == nil {
|
||||
t.Errorf("expected mod.Validate() error, but got none")
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectValidateErr && err != nil {
|
||||
t.Errorf("expected no error from mod.Validate(), but got: %v", err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngines_SystemMessages(t *testing.T) {
|
||||
mod := new(PDFEngines)
|
||||
func TestPdfEngines_SystemMessages(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.names = []string{"foo", "bar"}
|
||||
|
||||
messages := mod.SystemMessages()
|
||||
if len(messages) != 1 {
|
||||
t.Errorf("expected one and only one message from mod.SystemMessages(), but got %d", len(messages))
|
||||
t.Errorf("expected one and only one message, but got %d", len(messages))
|
||||
}
|
||||
|
||||
expect := strings.Join(mod.names[:], " ")
|
||||
if messages[0] != expect {
|
||||
t.Errorf("expected message '%s' from mod.SystemMessages(), but got '%s'", expect, messages[0])
|
||||
t.Errorf("expected message '%s', but got '%s'", expect, messages[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngines_PDFEngine(t *testing.T) {
|
||||
mod := PDFEngines{
|
||||
func TestPdfEngines_PdfEngine(t *testing.T) {
|
||||
mod := PdfEngines{
|
||||
names: []string{"foo", "bar"},
|
||||
engines: func() []gotenberg.PDFEngine {
|
||||
engines: func() []gotenberg.PdfEngine {
|
||||
engine1 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine1.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "foo", New: func() gotenberg.Module { return engine1 }}
|
||||
@@ -387,57 +387,53 @@ func TestPDFEngines_PDFEngine(t *testing.T) {
|
||||
|
||||
engine2 := &struct {
|
||||
gotenberg.ModuleMock
|
||||
gotenberg.PDFEngineMock
|
||||
gotenberg.PdfEngineMock
|
||||
}{}
|
||||
engine2.DescriptorMock = func() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{ID: "bar", New: func() gotenberg.Module { return engine2 }}
|
||||
}
|
||||
|
||||
return []gotenberg.PDFEngine{
|
||||
return []gotenberg.PdfEngine{
|
||||
engine1,
|
||||
engine2,
|
||||
}
|
||||
}(),
|
||||
}
|
||||
|
||||
_, err := mod.PDFEngine()
|
||||
_, err := mod.PdfEngine()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error from mod.PDFEngine, but got: %v", err)
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFEngines_Routes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mod PDFEngines
|
||||
expectRoutesCount int
|
||||
func TestPdfEngines_Routes(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
expectRoutes int
|
||||
disableRoutes bool
|
||||
}{
|
||||
{
|
||||
name: "route not disabled",
|
||||
mod: PDFEngines{
|
||||
engines: []gotenberg.PDFEngine{
|
||||
&gotenberg.PDFEngineMock{},
|
||||
},
|
||||
},
|
||||
expectRoutesCount: 2,
|
||||
scenario: "routes not disabled",
|
||||
expectRoutes: 2,
|
||||
disableRoutes: false,
|
||||
},
|
||||
{
|
||||
name: "route disabled",
|
||||
mod: PDFEngines{
|
||||
scenario: "routes disabled",
|
||||
expectRoutes: 0,
|
||||
disableRoutes: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
mod := new(PdfEngines)
|
||||
mod.disableRoutes = tc.disableRoutes
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
routes, err := tc.mod.Routes()
|
||||
routes, err := mod.Routes()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error from mod.Routes(), but got: %v", err)
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectRoutesCount != len(routes) {
|
||||
t.Errorf("expected %d routes from mod.Routes(), but got %d", tc.expectRoutesCount, len(routes))
|
||||
if tc.expectRoutes != len(routes) {
|
||||
t.Errorf("expected %d routes but got %d", tc.expectRoutes, len(routes))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||
)
|
||||
|
||||
// mergeRoute returns an api.Route which can merge PDFs.
|
||||
func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
||||
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/merge",
|
||||
@@ -23,17 +23,38 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
||||
// Let's get the data from the form and validate them.
|
||||
var (
|
||||
inputPaths []string
|
||||
PDFformat string
|
||||
pdfFormat string
|
||||
pdfa string
|
||||
pdfua bool
|
||||
)
|
||||
|
||||
err := ctx.FormData().
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
String("pdfFormat", &PDFformat, "").
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
var actualPdfArchive string
|
||||
|
||||
if pdfFormat != "" {
|
||||
// FIXME: deprecated
|
||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
||||
actualPdfArchive = pdfFormat
|
||||
}
|
||||
|
||||
if pdfa != "" {
|
||||
actualPdfArchive = pdfa
|
||||
}
|
||||
|
||||
pdfFormats := gotenberg.PdfFormats{
|
||||
PdfA: actualPdfArchive,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
|
||||
// Alright, let's merge the PDFs.
|
||||
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
@@ -45,21 +66,21 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
||||
|
||||
// So far so good, the PDFs are merged into one unique PDF.
|
||||
// Now, let's check if the client want to convert this result PDF
|
||||
// to a specific PDF format.
|
||||
|
||||
if PDFformat != "" {
|
||||
// to specific PDF formats.
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats != zeroValued {
|
||||
convertInputPath := outputPath
|
||||
convertOutputPath := ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), PDFformat, convertInputPath, convertOutputPath)
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, convertInputPath, convertOutputPath)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("convert PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -84,9 +105,9 @@ func mergeRoute(engine gotenberg.PDFEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// convertRoute returns an api.Route which can convert a PDF to a specific PDF
|
||||
// format.
|
||||
func convertRoute(engine gotenberg.PDFEngine) api.Route {
|
||||
// convertRoute returns an [api.Route] which can convert a PDF to a specific
|
||||
// PDF format.
|
||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/convert",
|
||||
@@ -97,33 +118,64 @@ func convertRoute(engine gotenberg.PDFEngine) api.Route {
|
||||
// Let's get the data from the form and validate them.
|
||||
var (
|
||||
inputPaths []string
|
||||
PDFformat string
|
||||
pdfFormat string
|
||||
pdfa string
|
||||
pdfua bool
|
||||
)
|
||||
|
||||
err := ctx.FormData().
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
MandatoryString("pdfFormat", &PDFformat).
|
||||
String("pdfFormat", &pdfFormat, "").
|
||||
String("pdfa", &pdfa, "").
|
||||
Bool("pdfua", &pdfua, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
// Alright, let's merge the PDFs.
|
||||
var actualPdfArchive string
|
||||
|
||||
if pdfFormat != "" {
|
||||
// FIXME: deprecated.
|
||||
ctx.Log().Warn("'pdfFormat' is deprecated; prefer the 'pdfa' form field instead")
|
||||
actualPdfArchive = pdfFormat
|
||||
}
|
||||
|
||||
if pdfa != "" {
|
||||
actualPdfArchive = pdfa
|
||||
}
|
||||
|
||||
pdfFormats := gotenberg.PdfFormats{
|
||||
PdfA: actualPdfArchive,
|
||||
PdfUa: pdfua,
|
||||
}
|
||||
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats == zeroValued {
|
||||
return api.WrapError(
|
||||
errors.New("no PDF formats"),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
"Invalid form data: either 'pdfa' or 'pdfua' form fields must be provided",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Alright, let's convert the PDFs.
|
||||
outputPaths := make([]string, len(inputPaths))
|
||||
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
|
||||
err = engine.Convert(ctx, ctx.Log(), PDFformat, inputPath, outputPaths[i])
|
||||
err = engine.Convert(ctx, ctx.Log(), pdfFormats, inputPath, outputPaths[i])
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gotenberg.ErrPDFFormatNotAvailable) {
|
||||
if errors.Is(err, gotenberg.ErrPdfFormatNotSupported) {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("convert PDF: %w", err),
|
||||
api.NewSentinelHTTPError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("At least one PDF engine does not handle the PDF format '%s' (pdfFormat), while other have failed to convert for other reasons", PDFformat),
|
||||
fmt.Sprintf("At least one PDF engine does not handle one of the PDF format in '%+v', while other have failed to convert for other reasons", pdfFormats),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,385 +14,408 @@ import (
|
||||
)
|
||||
|
||||
func TestMergeHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
engine gotenberg.PDFEngine
|
||||
expectErr bool
|
||||
expectHTTPErr bool
|
||||
expectHTTPStatus int
|
||||
engine gotenberg.PdfEngine
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
expectHttpStatus int
|
||||
expectOutputPathsCount int
|
||||
}{
|
||||
{
|
||||
name: "nominal behavior",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectOutputPathsCount: 1,
|
||||
scenario: "missing at least one mandatory file",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid form data: no PDF",
|
||||
ctx: &api.ContextMock{Context: &api.Context{}},
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "merge fail",
|
||||
scenario: "error from PDF engine",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "nominal behavior with a PDF format",
|
||||
scenario: "cannot add output paths",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
name: "convert to PDF format fail",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid PDF format",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "cannot add output paths",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
{
|
||||
scenario: "success",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "ErrPdfFormatNotSupported",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPdfFormatNotSupported
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine (convert)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "success with every PDF/A & PDF/UA form fields",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
c := echo.New().NewContext(nil, nil)
|
||||
c.Set("context", tc.ctx.Context)
|
||||
|
||||
err := mergeRoute(tc.engine).Handler(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Fatal("expected error from merge handler, but got none")
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Fatalf("expected no error from merge handler, but got: %v", err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
var httpErr api.HTTPError
|
||||
isHTTPErr := errors.As(err, &httpErr)
|
||||
|
||||
if tc.expectHTTPErr && !isHTTPErr {
|
||||
t.Errorf("expected HTTP error from merge handler, but got: %v", err)
|
||||
if tc.expectHttpError && !isHTTPErr {
|
||||
t.Errorf("expected an HTTP error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectHTTPErr && isHTTPErr {
|
||||
t.Errorf("expected no HTTP error from merge handler, but got one: %v", httpErr)
|
||||
if !tc.expectHttpError && isHTTPErr {
|
||||
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||
}
|
||||
|
||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
||||
if err != nil && tc.expectHttpError && isHTTPErr {
|
||||
status, _ := httpErr.HTTPError()
|
||||
if status != tc.expectHTTPStatus {
|
||||
t.Errorf("expected %d HTTP status code from merge handler, but got %d", tc.expectHTTPStatus, status)
|
||||
if status != tc.expectHttpStatus {
|
||||
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||
t.Errorf("expected %d output paths from merge handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertHandler(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx *api.ContextMock
|
||||
engine gotenberg.PDFEngine
|
||||
expectErr bool
|
||||
expectHTTPErr bool
|
||||
expectHTTPStatus int
|
||||
engine gotenberg.PdfEngine
|
||||
expectError bool
|
||||
expectHttpError bool
|
||||
expectHttpStatus int
|
||||
expectOutputPathsCount int
|
||||
}{
|
||||
{
|
||||
name: "nominal behavior",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectOutputPathsCount: 1,
|
||||
scenario: "missing at least one mandatory file",
|
||||
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "nominal behavior, but with 3 PDFs",
|
||||
scenario: "no PDF formats",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
"bar.pdf": "/bar/bar.pdf",
|
||||
"baz.pdf": "/baz/baz.pdf",
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectOutputPathsCount: 3,
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "invalid form data: no PDF",
|
||||
scenario: "ErrPdfFormatNotSupported",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid form data: no PDF format",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "convert to PDF format fail",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPdfFormatNotSupported
|
||||
},
|
||||
},
|
||||
expectError: true,
|
||||
expectHttpError: true,
|
||||
expectHttpStatus: http.StatusBadRequest,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
scenario: "error from PDF engine",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "PDF format not available",
|
||||
scenario: "cannot add output paths",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
"foo",
|
||||
},
|
||||
})
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
return gotenberg.ErrPDFFormatNotAvailable
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
expectHTTPErr: true,
|
||||
expectHTTPStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "cannot add output paths",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: &api.Context{}}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"foo.pdf": "/foo/foo.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.FormatPDFA1a,
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
})
|
||||
ctx.SetCancelled(true)
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PDFEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, format, inputPath, outputPath string) error {
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
{
|
||||
scenario: "success with every PDF/A & PDF/UA form fields (single file)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 1,
|
||||
},
|
||||
{
|
||||
scenario: "success with every PDF/A & PDF/UA form fields (many files)",
|
||||
ctx: func() *api.ContextMock {
|
||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||
ctx.SetFiles(map[string]string{
|
||||
"file.pdf": "/file.pdf",
|
||||
"file2.pdf": "/file2.pdf",
|
||||
})
|
||||
ctx.SetValues(map[string][]string{
|
||||
"pdfFormat": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfa": {
|
||||
gotenberg.PdfA1a,
|
||||
},
|
||||
"pdfua": {
|
||||
"true",
|
||||
},
|
||||
})
|
||||
return ctx
|
||||
}(),
|
||||
engine: &gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
expectHttpError: false,
|
||||
expectOutputPathsCount: 2,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
tc.ctx.SetLogger(zap.NewNop())
|
||||
c := echo.New().NewContext(nil, nil)
|
||||
c.Set("context", tc.ctx.Context)
|
||||
|
||||
err := convertRoute(tc.engine).Handler(c)
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Fatal("expected error from convert handler, but got none")
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Fatalf("expected no error from convert handler, but got: %v", err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
var httpErr api.HTTPError
|
||||
isHTTPErr := errors.As(err, &httpErr)
|
||||
|
||||
if tc.expectHTTPErr && !isHTTPErr {
|
||||
t.Errorf("expected HTTP error from convert handler, but got: %v", err)
|
||||
if tc.expectHttpError && !isHTTPErr {
|
||||
t.Errorf("expected an HTTP error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectHTTPErr && isHTTPErr {
|
||||
t.Errorf("expected no HTTP error from convert handler, but got one: %v", httpErr)
|
||||
if !tc.expectHttpError && isHTTPErr {
|
||||
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||
}
|
||||
|
||||
if err != nil && tc.expectHTTPErr && isHTTPErr {
|
||||
if err != nil && tc.expectHttpError && isHTTPErr {
|
||||
status, _ := httpErr.HTTPError()
|
||||
if status != tc.expectHTTPStatus {
|
||||
t.Errorf("expected %d HTTP status code from convert handler, but got %d", tc.expectHTTPStatus, status)
|
||||
if status != tc.expectHttpStatus {
|
||||
t.Errorf("expected %d as HTTP status code but got %d", tc.expectHttpStatus, status)
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectOutputPathsCount != len(tc.ctx.OutputPaths()) {
|
||||
t.Errorf("expected %d output paths from convert handler, but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||
t.Errorf("expected %d output paths but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// Package pdftk provides a module which abstracts the CLI tool PDFtk and
|
||||
// implements the gotenberg.PDFEngine interface.
|
||||
// Package pdftk provides an implementation of the gotenberg.PdfEngine
|
||||
// interface using the PDFtk command-line tool. This package allows for the
|
||||
// merging of PDF files but does not support conversion to specific PDF
|
||||
// formats. The path to the PDFtk binary must be specified using the
|
||||
// PDFTK_BIN_PATH environment variable.
|
||||
//
|
||||
// See: https://gitlab.com/pdftk-java/pdftk.
|
||||
package pdftk
|
||||
|
||||
@@ -13,26 +13,25 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
gotenberg.MustRegisterModule(PDFtk{})
|
||||
gotenberg.MustRegisterModule(new(PdfTk))
|
||||
}
|
||||
|
||||
// PDFtk abstracts the CLI tool PDFtk and implements the gotenberg.PDFEngine
|
||||
// PdfTk abstracts the CLI tool PDFtk and implements the [gotenberg.PdfEngine]
|
||||
// interface.
|
||||
type PDFtk struct {
|
||||
type PdfTk struct {
|
||||
binPath string
|
||||
}
|
||||
|
||||
// Descriptor returns a PDFtk's module descriptor.
|
||||
func (PDFtk) Descriptor() gotenberg.ModuleDescriptor {
|
||||
// Descriptor returns a [PdfTk]'s module descriptor.
|
||||
func (engine *PdfTk) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
ID: "pdftk",
|
||||
New: func() gotenberg.Module { return new(PDFtk) },
|
||||
New: func() gotenberg.Module { return new(PdfTk) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets the modules properties. It returns an error if the
|
||||
// environment variable PDFTK_BIN_PATH is not set.
|
||||
func (engine *PDFtk) Provision(_ *gotenberg.Context) error {
|
||||
// Provision sets the modules properties.
|
||||
func (engine *PdfTk) Provision(ctx *gotenberg.Context) error {
|
||||
binPath, ok := os.LookupEnv("PDFTK_BIN_PATH")
|
||||
if !ok {
|
||||
return errors.New("PDFTK_BIN_PATH environment variable is not set")
|
||||
@@ -44,21 +43,22 @@ func (engine *PDFtk) Provision(_ *gotenberg.Context) error {
|
||||
}
|
||||
|
||||
// Validate validates the module properties.
|
||||
func (engine PDFtk) Validate() error {
|
||||
func (engine *PdfTk) Validate() error {
|
||||
_, err := os.Stat(engine.binPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("PDFtk binary path does not exist: %w", err)
|
||||
return fmt.Errorf("PdfTk binary path does not exist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Metrics returns the metrics.
|
||||
func (engine PDFtk) Metrics() ([]gotenberg.Metric, error) {
|
||||
func (engine *PdfTk) Metrics() ([]gotenberg.Metric, error) {
|
||||
// TODO: remove deprecated.
|
||||
return []gotenberg.Metric{
|
||||
{
|
||||
Name: "pdftk_active_instances_count",
|
||||
Description: "Current number of active PDFtk instances.",
|
||||
Description: "Current number of active PDFtk instances - deprecated.",
|
||||
Read: func() float64 {
|
||||
activeInstancesCountMu.RLock()
|
||||
defer activeInstancesCountMu.RUnlock()
|
||||
@@ -69,8 +69,8 @@ func (engine PDFtk) Metrics() ([]gotenberg.Metric, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Merge merges the given PDFs into a unique PDF.
|
||||
func (engine PDFtk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
args = append(args, inputPaths...)
|
||||
args = append(args, "cat", "output", outputPath)
|
||||
@@ -97,9 +97,9 @@ func (engine PDFtk) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
|
||||
return fmt.Errorf("merge PDFs with PDFtk: %w", err)
|
||||
}
|
||||
|
||||
// Convert is not available for this PDF engine.
|
||||
func (engine PDFtk) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
|
||||
return fmt.Errorf("convert PDF to '%s' with PDFtk: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -109,9 +109,9 @@ var (
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*PDFtk)(nil)
|
||||
_ gotenberg.Provisioner = (*PDFtk)(nil)
|
||||
_ gotenberg.Validator = (*PDFtk)(nil)
|
||||
_ gotenberg.MetricsProvider = (*PDFtk)(nil)
|
||||
_ gotenberg.PDFEngine = (*PDFtk)(nil)
|
||||
_ gotenberg.Module = (*PdfTk)(nil)
|
||||
_ gotenberg.Provisioner = (*PdfTk)(nil)
|
||||
_ gotenberg.Validator = (*PdfTk)(nil)
|
||||
_ gotenberg.MetricsProvider = (*PdfTk)(nil)
|
||||
_ gotenberg.PdfEngine = (*PdfTk)(nil)
|
||||
)
|
||||
|
||||
@@ -12,59 +12,67 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestPDFtk_Descriptor(t *testing.T) {
|
||||
descriptor := PDFtk{}.Descriptor()
|
||||
func TestPdfTk_Descriptor(t *testing.T) {
|
||||
descriptor := new(PdfTk).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(PDFtk))
|
||||
expect := reflect.TypeOf(new(PdfTk))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFtk_Provision(t *testing.T) {
|
||||
mod := new(PDFtk)
|
||||
func TestPdfTk_Provision(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||
|
||||
err := mod.Provision(ctx)
|
||||
err := engine.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFtk_Validate(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
func TestPdfTk_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
expectErr: true,
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("PDFTK_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
mod := new(PDFtk)
|
||||
mod.binPath = tc.binPath
|
||||
err := mod.Validate()
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
engine.binPath = tc.binPath
|
||||
err := engine.Validate()
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFtk_Metrics(t *testing.T) {
|
||||
metrics, err := new(PDFtk).Metrics()
|
||||
func TestPdfTk_Metrics(t *testing.T) {
|
||||
metrics, err := new(PdfTk).Metrics()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
@@ -79,76 +87,80 @@ func TestPDFtk_Metrics(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFtk_Merge(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
func TestPdfTk_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
inputPaths []string
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "single file success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
ctx: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
mod := new(PDFtk)
|
||||
|
||||
err := mod.Provision(nil)
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem()
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(fs.WorkingDirPath())
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||
err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPDFtk_Convert(t *testing.T) {
|
||||
mod := new(PDFtk)
|
||||
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
|
||||
func TestPdfTk_Convert(t *testing.T) {
|
||||
engine := new(PdfTk)
|
||||
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
// Package qpdf provides a module which abstracts the CLI tool QPDF and
|
||||
// implements the gotenberg.PDFEngine interface.
|
||||
// Package qpdf provides an implementation of the gotenberg.PdfEngine
|
||||
// interface using the QPDF command-line tool. This package allows for the
|
||||
// merging of PDF files but does not support conversion to specific PDF
|
||||
// formats. The path to the QPDF binary must be specified using the
|
||||
// QPDK_BIN_PATH environment variable.
|
||||
//
|
||||
// See: https://github.com/qpdf/qpdf.
|
||||
package qpdf
|
||||
|
||||
@@ -13,26 +13,25 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
gotenberg.MustRegisterModule(QPDF{})
|
||||
gotenberg.MustRegisterModule(new(QPdf))
|
||||
}
|
||||
|
||||
// QPDF abstracts the CLI tool QPDF and implements the gotenberg.PDFEngine
|
||||
// QPdf abstracts the CLI tool QPDF and implements the [gotenberg.PdfEngine]
|
||||
// interface.
|
||||
type QPDF struct {
|
||||
type QPdf struct {
|
||||
binPath string
|
||||
}
|
||||
|
||||
// Descriptor returns a QPDF's module descriptor.
|
||||
func (QPDF) Descriptor() gotenberg.ModuleDescriptor {
|
||||
// Descriptor returns a [QPdf]'s module descriptor.
|
||||
func (engine *QPdf) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
ID: "qpdf",
|
||||
New: func() gotenberg.Module { return new(QPDF) },
|
||||
New: func() gotenberg.Module { return new(QPdf) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets the modules properties. It returns an error if the
|
||||
// environment variable QPDF_BIN_PATH is not set.
|
||||
func (engine *QPDF) Provision(_ *gotenberg.Context) error {
|
||||
// Provision sets the modules properties.
|
||||
func (engine *QPdf) Provision(ctx *gotenberg.Context) error {
|
||||
binPath, ok := os.LookupEnv("QPDF_BIN_PATH")
|
||||
if !ok {
|
||||
return errors.New("QPDF_BIN_PATH environment variable is not set")
|
||||
@@ -44,21 +43,22 @@ func (engine *QPDF) Provision(_ *gotenberg.Context) error {
|
||||
}
|
||||
|
||||
// Validate validates the module properties.
|
||||
func (engine QPDF) Validate() error {
|
||||
func (engine *QPdf) Validate() error {
|
||||
_, err := os.Stat(engine.binPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("QPDF binary path does not exist: %w", err)
|
||||
return fmt.Errorf("QPdf binary path does not exist: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Metrics returns the metrics.
|
||||
func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
|
||||
func (engine *QPdf) Metrics() ([]gotenberg.Metric, error) {
|
||||
// TODO: remove deprecated.
|
||||
return []gotenberg.Metric{
|
||||
{
|
||||
Name: "qpdf_active_instances_count",
|
||||
Description: "Current number of active QPDF instances.",
|
||||
Description: "Current number of active QPDF instances - deprecated.",
|
||||
Read: func() float64 {
|
||||
activeInstancesCountMu.RLock()
|
||||
defer activeInstancesCountMu.RUnlock()
|
||||
@@ -69,8 +69,8 @@ func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Merge merges the given PDFs into a unique PDF.
|
||||
func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
// Merge combines multiple PDFs into a single PDF.
|
||||
func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
var args []string
|
||||
args = append(args, "--empty")
|
||||
args = append(args, "--pages")
|
||||
@@ -99,9 +99,9 @@ func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []s
|
||||
return fmt.Errorf("merge PDFs with QPDF: %w", err)
|
||||
}
|
||||
|
||||
// Convert is not available for this PDF engine.
|
||||
func (engine QPDF) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
|
||||
return fmt.Errorf("convert PDF to '%s' with QPDF: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||
// Convert is not available in this implementation.
|
||||
func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -110,9 +110,9 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
_ gotenberg.Module = (*QPDF)(nil)
|
||||
_ gotenberg.Provisioner = (*QPDF)(nil)
|
||||
_ gotenberg.Validator = (*QPDF)(nil)
|
||||
_ gotenberg.MetricsProvider = (*QPDF)(nil)
|
||||
_ gotenberg.PDFEngine = (*QPDF)(nil)
|
||||
_ gotenberg.Module = (*QPdf)(nil)
|
||||
_ gotenberg.Provisioner = (*QPdf)(nil)
|
||||
_ gotenberg.Validator = (*QPdf)(nil)
|
||||
_ gotenberg.MetricsProvider = (*QPdf)(nil)
|
||||
_ gotenberg.PdfEngine = (*QPdf)(nil)
|
||||
)
|
||||
|
||||
@@ -12,59 +12,67 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestQPDF_Descriptor(t *testing.T) {
|
||||
descriptor := QPDF{}.Descriptor()
|
||||
func TestQPdf_Descriptor(t *testing.T) {
|
||||
descriptor := new(QPdf).Descriptor()
|
||||
|
||||
actual := reflect.TypeOf(descriptor.New())
|
||||
expect := reflect.TypeOf(new(QPDF))
|
||||
expect := reflect.TypeOf(new(QPdf))
|
||||
|
||||
if actual != expect {
|
||||
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPDF_Provision(t *testing.T) {
|
||||
mod := new(QPDF)
|
||||
func TestQPdf_Provision(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||
|
||||
err := mod.Provision(ctx)
|
||||
err := engine.Provision(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPDF_Validate(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
func TestQPdf_Validate(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
binPath string
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
expectErr: true,
|
||||
scenario: "empty bin path",
|
||||
binPath: "",
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "bin path does not exist",
|
||||
binPath: "/foo",
|
||||
expectErr: true,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "validate success",
|
||||
binPath: os.Getenv("QPDF_BIN_PATH"),
|
||||
expectError: false,
|
||||
},
|
||||
} {
|
||||
mod := new(QPDF)
|
||||
mod.binPath = tc.binPath
|
||||
err := mod.Validate()
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
engine.binPath = tc.binPath
|
||||
err := engine.Validate()
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPDF_Metrics(t *testing.T) {
|
||||
metrics, err := new(QPDF).Metrics()
|
||||
func TestQPdf_Metrics(t *testing.T) {
|
||||
metrics, err := new(QPdf).Metrics()
|
||||
if err != nil {
|
||||
t.Errorf("expected no error but got: %v", err)
|
||||
}
|
||||
@@ -75,80 +83,84 @@ func TestQPDF_Metrics(t *testing.T) {
|
||||
|
||||
actual := metrics[0].Read()
|
||||
if actual != 0 {
|
||||
t.Errorf("expected %d QPDF instances, but got %f", 0, actual)
|
||||
t.Errorf("expected %d QPdf instances, but got %f", 0, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPDF_Merge(t *testing.T) {
|
||||
for i, tc := range []struct {
|
||||
func TestQPdf_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
ctx context.Context
|
||||
inputPaths []string
|
||||
expectErr bool
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "invalid context",
|
||||
ctx: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "invalid input path",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "single file success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: "many files success",
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||
},
|
||||
},
|
||||
{
|
||||
ctx: nil,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
ctx: context.TODO(),
|
||||
inputPaths: []string{
|
||||
"foo",
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
func() {
|
||||
mod := new(QPDF)
|
||||
|
||||
err := mod.Provision(nil)
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
err := engine.Provision(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
fs := gotenberg.NewFileSystem()
|
||||
outputDir, err := fs.MkdirAll()
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||
t.Fatalf("expected error but got: %v", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.RemoveAll(fs.WorkingDirPath())
|
||||
err = os.RemoveAll(fs.WorkingDirPath())
|
||||
if err != nil {
|
||||
t.Fatalf("test %d: expected no error while cleaning up but got: %v", i, err)
|
||||
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||
err = engine.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||
|
||||
if tc.expectErr && err == nil {
|
||||
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if !tc.expectErr && err != nil {
|
||||
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQPDF_Convert(t *testing.T) {
|
||||
mod := new(QPDF)
|
||||
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
|
||||
func TestQPdf_Convert(t *testing.T) {
|
||||
engine := new(QPdf)
|
||||
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user