mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-18 13:12:17 +01:00
feat(pdfengines): add support for flattening annotations (#1105)
* initial changes
* Add tests
* Fix edge case when we need to regenerate appearances
* Fix comments
* Add missing comment
* Add missing comment
* Add missing comment
* Add flatten option to the merge route
* Add flatten option to the libreoffice convert route
* Add flatten option to the chromium convert route
* Revert "Add flatten option to the chromium convert route"
This reverts commit cdab8b4e6b.
* Ignore lint false positives
* Add missing tests
* Add flatten route tests
* Replace input instead of creating a new file
* create copy before flatten in tests
---------
Co-authored-by: Peter Chakalov <peter.chakalov@abraxa.com>
This commit is contained in:
2
Makefile
2
Makefile
@@ -65,6 +65,7 @@ LOG_FIELDS_PREFIX=
|
|||||||
PDFENGINES_ENGINES=
|
PDFENGINES_ENGINES=
|
||||||
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
||||||
PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk
|
PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk
|
||||||
|
PDFENGINES_FLATTEN_ENGINES=qpdf
|
||||||
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
|
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
|
||||||
PDFENGINES_READ_METADATA_ENGINES=exiftool
|
PDFENGINES_READ_METADATA_ENGINES=exiftool
|
||||||
PDFENGINES_WRITE_METADATA_ENGINES=exiftool
|
PDFENGINES_WRITE_METADATA_ENGINES=exiftool
|
||||||
@@ -134,6 +135,7 @@ run: ## Start a Gotenberg container
|
|||||||
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
--pdfengines-engines=$(PDFENGINES_ENGINES) \
|
||||||
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||||
--pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \
|
--pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \
|
||||||
|
--pdfengines-convert-engines=$(PDFENGINES_FLATTEN_ENGINES) \
|
||||||
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
|
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
|
||||||
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
|
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
|
||||||
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
|
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
|
||||||
|
|||||||
@@ -35,9 +35,12 @@ func (mod *ValidatorMock) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
// PdfEngineMock is a mock for the [PdfEngine] interface.
|
||||||
|
//
|
||||||
|
//nolint:dupl
|
||||||
type PdfEngineMock struct {
|
type PdfEngineMock struct {
|
||||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||||
SplitMock func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
SplitMock func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||||
|
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error)
|
||||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error
|
||||||
@@ -51,6 +54,10 @@ func (engine *PdfEngineMock) Split(ctx context.Context, logger *zap.Logger, mode
|
|||||||
return engine.SplitMock(ctx, logger, mode, inputPath, outputDirPath)
|
return engine.SplitMock(ctx, logger, mode, inputPath, outputDirPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return engine.FlattenMock(ctx, logger, inputPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||||
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
return engine.ConvertMock(ctx, logger, formats, inputPath, outputPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ func TestPDFEngineMock(t *testing.T) {
|
|||||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
SplitMock: func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -77,6 +80,11 @@ func TestPDFEngineMock(t *testing.T) {
|
|||||||
t.Errorf("expected no error from PdfEngineMock.Split, but got: %v", err)
|
t.Errorf("expected no error from PdfEngineMock.Split, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mock.Flatten(context.Background(), zap.NewNop(), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
|
err = mock.Convert(context.Background(), zap.NewNop(), PdfFormats{}, "", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
t.Errorf("expected no error from PdfEngineMock.Convert, but got: %v", err)
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ type PdfFormats struct {
|
|||||||
// PdfEngine provides an interface for operations on PDFs. Implementations
|
// PdfEngine provides an interface for operations on PDFs. Implementations
|
||||||
// can utilize various tools like PDFtk, or implement functionality directly in
|
// can utilize various tools like PDFtk, or implement functionality directly in
|
||||||
// Go.
|
// Go.
|
||||||
|
//
|
||||||
|
//nolint:dupl
|
||||||
type PdfEngine interface {
|
type PdfEngine interface {
|
||||||
// Merge combines multiple PDFs into a single PDF. The resulting page order
|
// Merge combines multiple PDFs into a single PDF. The resulting page order
|
||||||
// is determined by the order of files provided in inputPaths.
|
// is determined by the order of files provided in inputPaths.
|
||||||
@@ -96,6 +98,11 @@ type PdfEngine interface {
|
|||||||
// Split splits a given PDF file.
|
// Split splits a given PDF file.
|
||||||
Split(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
Split(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||||
|
|
||||||
|
// Flatten merges existing annotation appearances with page content, effectively deleting the original annotations.
|
||||||
|
// This process can flatten forms as well, as forms share a relationship with annotations.
|
||||||
|
// Note that this operation is irreversible.
|
||||||
|
Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||||
|
|
||||||
// Convert transforms a given PDF to the specified formats defined in
|
// Convert transforms a given PDF to the specified formats defined in
|
||||||
// PdfFormats. If no format, it does nothing.
|
// PdfFormats. If no format, it does nothing.
|
||||||
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
Convert(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ func (engine *ExifTool) Split(ctx context.Context, logger *zap.Logger, mode gote
|
|||||||
return nil, fmt.Errorf("split PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
return nil, fmt.Errorf("split PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten is not available in this implementation.
|
||||||
|
func (engine *ExifTool) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return fmt.Errorf("flatten PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert is not available in this implementation.
|
// Convert is not available in this implementation.
|
||||||
func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with ExifTool: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -91,6 +91,15 @@ func TestExiftool_Split(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExiftool_Flatten(t *testing.T) {
|
||||||
|
engine := new(ExifTool)
|
||||||
|
err := engine.Flatten(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExiftool_Convert(t *testing.T) {
|
func TestExiftool_Convert(t *testing.T) {
|
||||||
engine := new(ExifTool)
|
engine := new(ExifTool)
|
||||||
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
err := engine.Convert(context.Background(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ func (engine *LibreOfficePdfEngine) Split(ctx context.Context, logger *zap.Logge
|
|||||||
return nil, fmt.Errorf("split PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
return nil, fmt.Errorf("split PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten is not available in this implementation.
|
||||||
|
func (engine *LibreOfficePdfEngine) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return fmt.Errorf("Flatten PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
// Convert converts the given PDF to a specific PDF format. Currently, only the
|
||||||
// PDF/A-1b, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
// PDF/A-1b, PDF/A-2b, PDF/A-3b and PDF/UA formats are available. If another
|
||||||
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
// PDF format is requested, it returns a [gotenberg.ErrPdfFormatNotSupported]
|
||||||
|
|||||||
@@ -127,6 +127,15 @@ func TestLibreOfficePdfEngine_Split(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLibreOfficePdfEngine_Flatten(t *testing.T) {
|
||||||
|
engine := new(LibreOfficePdfEngine)
|
||||||
|
err := engine.Flatten(context.Background(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
func TestLibreOfficePdfEngine_Convert(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
maxImageResolution int
|
maxImageResolution int
|
||||||
nativePdfFormats bool
|
nativePdfFormats bool
|
||||||
merge bool
|
merge bool
|
||||||
|
flatten bool
|
||||||
)
|
)
|
||||||
|
|
||||||
err := form.
|
err := form.
|
||||||
@@ -135,6 +136,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
|
Bool("flatten", &flatten, false).
|
||||||
Validate()
|
Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
@@ -261,6 +263,13 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
return fmt.Errorf("write metadata: %w", err)
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flatten {
|
||||||
|
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("flatten PDFs: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(outputPaths) > 1 && splitMode == zeroValuedSplitMode {
|
if len(outputPaths) > 1 && splitMode == zeroValuedSplitMode {
|
||||||
// If .zip archive, document.docx -> document.docx.pdf.
|
// If .zip archive, document.docx -> document.docx.pdf.
|
||||||
for i, inputPath := range inputPaths {
|
for i, inputPath := range inputPaths {
|
||||||
|
|||||||
@@ -402,6 +402,38 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine flatten error",
|
||||||
|
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{
|
||||||
|
"flatten": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
ctx.SetCancelled(true)
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
libreOffice: &libreofficeapi.ApiMock{
|
||||||
|
PdfMock: func(ctx context.Context, logger *zap.Logger, inputPath, outputPath string, options libreofficeapi.Options) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExtensionsMock: func() []string {
|
||||||
|
return []string{".docx"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths",
|
scenario: "cannot add output paths",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -454,6 +486,9 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
},
|
},
|
||||||
|
"flatten": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -475,6 +510,9 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
@@ -502,6 +540,9 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
},
|
},
|
||||||
|
"flatten": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
ctx.SetPathRename(&gotenberg.PathRenameMock{RenameMock: func(oldpath, newpath string) error {
|
||||||
return nil
|
return nil
|
||||||
@@ -526,6 +567,9 @@ func TestConvertRoute(t *testing.T) {
|
|||||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb
|
|||||||
return outputPaths, nil
|
return outputPaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten is not available in this implementation.
|
||||||
|
func (engine *PdfCpu) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return fmt.Errorf("flatten PDF with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert is not available in this implementation.
|
// Convert is not available in this implementation.
|
||||||
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return fmt.Errorf("convert PDF to '%+v' with pdfcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with pdfcpu: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -239,6 +239,15 @@ func TestPdfCpu_Split(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPdfCpu_Flatten(t *testing.T) {
|
||||||
|
mod := new(PdfCpu)
|
||||||
|
err := mod.Flatten(context.TODO(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPdfCpu_Convert(t *testing.T) {
|
func TestPdfCpu_Convert(t *testing.T) {
|
||||||
mod := new(PdfCpu)
|
mod := new(PdfCpu)
|
||||||
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
err := mod.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
type multiPdfEngines struct {
|
type multiPdfEngines struct {
|
||||||
mergeEngines []gotenberg.PdfEngine
|
mergeEngines []gotenberg.PdfEngine
|
||||||
splitEngines []gotenberg.PdfEngine
|
splitEngines []gotenberg.PdfEngine
|
||||||
|
flattenEngines []gotenberg.PdfEngine
|
||||||
convertEngines []gotenberg.PdfEngine
|
convertEngines []gotenberg.PdfEngine
|
||||||
readMetadataEngines []gotenberg.PdfEngine
|
readMetadataEngines []gotenberg.PdfEngine
|
||||||
writeMetadataEngines []gotenberg.PdfEngine
|
writeMetadataEngines []gotenberg.PdfEngine
|
||||||
@@ -22,6 +23,7 @@ type multiPdfEngines struct {
|
|||||||
func newMultiPdfEngines(
|
func newMultiPdfEngines(
|
||||||
mergeEngines,
|
mergeEngines,
|
||||||
splitEngines,
|
splitEngines,
|
||||||
|
flattenEngines,
|
||||||
convertEngines,
|
convertEngines,
|
||||||
readMetadataEngines,
|
readMetadataEngines,
|
||||||
writeMetadataEngines []gotenberg.PdfEngine,
|
writeMetadataEngines []gotenberg.PdfEngine,
|
||||||
@@ -29,6 +31,7 @@ func newMultiPdfEngines(
|
|||||||
return &multiPdfEngines{
|
return &multiPdfEngines{
|
||||||
mergeEngines: mergeEngines,
|
mergeEngines: mergeEngines,
|
||||||
splitEngines: splitEngines,
|
splitEngines: splitEngines,
|
||||||
|
flattenEngines: flattenEngines,
|
||||||
convertEngines: convertEngines,
|
convertEngines: convertEngines,
|
||||||
readMetadataEngines: readMetadataEngines,
|
readMetadataEngines: readMetadataEngines,
|
||||||
writeMetadataEngines: writeMetadataEngines,
|
writeMetadataEngines: writeMetadataEngines,
|
||||||
@@ -98,6 +101,32 @@ func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mod
|
|||||||
return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err)
|
return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten merges existing annotation appearances with page content, effectively deleting the original annotations.
|
||||||
|
// This process can flatten forms as well, as forms share a relationship with annotations.
|
||||||
|
// Note that this operation is irreversible.
|
||||||
|
func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
var err error
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
for _, engine := range multi.flattenEngines {
|
||||||
|
go func(engine gotenberg.PdfEngine) {
|
||||||
|
errChan <- engine.Flatten(ctx, logger, inputPath)
|
||||||
|
}(engine)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case mergeErr := <-errChan:
|
||||||
|
errored := multierr.AppendInto(&err, mergeErr)
|
||||||
|
if !errored {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert converts the given PDF to a specific PDF format. thanks to its
|
// Convert converts the given PDF to a specific PDF format. thanks to its
|
||||||
// children. If the context is done, it stops and returns an error.
|
// children. If the context is done, it stops and returns an error.
|
||||||
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
|
|||||||
@@ -194,6 +194,97 @@ func TestMultiPdfEngines_Split(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMultiPdfEngines_Flatten(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
engine *multiPdfEngines
|
||||||
|
ctx context.Context
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "nominal behavior",
|
||||||
|
engine: &multiPdfEngines{
|
||||||
|
flattenEngines: []gotenberg.PdfEngine{
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "at least one engine does not return an error",
|
||||||
|
engine: &multiPdfEngines{
|
||||||
|
flattenEngines: []gotenberg.PdfEngine{
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "all engines return an error",
|
||||||
|
engine: &multiPdfEngines{
|
||||||
|
flattenEngines: []gotenberg.PdfEngine{
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: context.Background(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "context expired",
|
||||||
|
engine: &multiPdfEngines{
|
||||||
|
flattenEngines: []gotenberg.PdfEngine{
|
||||||
|
&gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ctx: func() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
err := tc.engine.Flatten(tc.ctx, zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMultiPdfEngines_Convert(t *testing.T) {
|
func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ func init() {
|
|||||||
type PdfEngines struct {
|
type PdfEngines struct {
|
||||||
mergeNames []string
|
mergeNames []string
|
||||||
splitNames []string
|
splitNames []string
|
||||||
|
flattenNames []string
|
||||||
convertNames []string
|
convertNames []string
|
||||||
readMetadataNames []string
|
readMetadataNames []string
|
||||||
writeMetadataNames []string
|
writeMetadataNames []string
|
||||||
@@ -44,6 +45,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
||||||
fs.StringSlice("pdfengines-merge-engines", []string{"qpdf", "pdfcpu", "pdftk"}, "Set the PDF engines and their order for the merge feature - empty means all")
|
fs.StringSlice("pdfengines-merge-engines", []string{"qpdf", "pdfcpu", "pdftk"}, "Set the PDF engines and their order for the merge feature - empty means all")
|
||||||
fs.StringSlice("pdfengines-split-engines", []string{"pdfcpu", "qpdf", "pdftk"}, "Set the PDF engines and their order for the split feature - empty means all")
|
fs.StringSlice("pdfengines-split-engines", []string{"pdfcpu", "qpdf", "pdftk"}, "Set the PDF engines and their order for the split feature - empty means all")
|
||||||
|
fs.StringSlice("pdfengines-flatten-engines", []string{"qpdf"}, "Set the PDF engines and their order for the flatten feature - empty means all")
|
||||||
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
|
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
|
||||||
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
|
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
|
||||||
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
||||||
@@ -67,6 +69,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
flags := ctx.ParsedFlags()
|
flags := ctx.ParsedFlags()
|
||||||
mergeNames := flags.MustStringSlice("pdfengines-merge-engines")
|
mergeNames := flags.MustStringSlice("pdfengines-merge-engines")
|
||||||
splitNames := flags.MustStringSlice("pdfengines-split-engines")
|
splitNames := flags.MustStringSlice("pdfengines-split-engines")
|
||||||
|
flattenNames := flags.MustStringSlice("pdfengines-flatten-engines")
|
||||||
convertNames := flags.MustStringSlice("pdfengines-convert-engines")
|
convertNames := flags.MustStringSlice("pdfengines-convert-engines")
|
||||||
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
|
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
|
||||||
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
||||||
@@ -106,6 +109,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
mod.splitNames = splitNames
|
mod.splitNames = splitNames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod.flattenNames = defaultNames
|
||||||
|
if len(flattenNames) > 0 {
|
||||||
|
mod.flattenNames = flattenNames
|
||||||
|
}
|
||||||
|
|
||||||
mod.convertNames = defaultNames
|
mod.convertNames = defaultNames
|
||||||
if len(convertNames) > 0 {
|
if len(convertNames) > 0 {
|
||||||
mod.convertNames = convertNames
|
mod.convertNames = convertNames
|
||||||
@@ -170,6 +178,7 @@ func (mod *PdfEngines) Validate() error {
|
|||||||
|
|
||||||
findNonExistingEngines(mod.mergeNames)
|
findNonExistingEngines(mod.mergeNames)
|
||||||
findNonExistingEngines(mod.splitNames)
|
findNonExistingEngines(mod.splitNames)
|
||||||
|
findNonExistingEngines(mod.flattenNames)
|
||||||
findNonExistingEngines(mod.convertNames)
|
findNonExistingEngines(mod.convertNames)
|
||||||
findNonExistingEngines(mod.readMetadataNames)
|
findNonExistingEngines(mod.readMetadataNames)
|
||||||
findNonExistingEngines(mod.writeMetadataNames)
|
findNonExistingEngines(mod.writeMetadataNames)
|
||||||
@@ -187,6 +196,7 @@ func (mod *PdfEngines) SystemMessages() []string {
|
|||||||
return []string{
|
return []string{
|
||||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||||
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
||||||
|
fmt.Sprintf("flatten engines - %s", strings.Join(mod.flattenNames[:], " ")),
|
||||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||||
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
||||||
@@ -212,6 +222,7 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
|||||||
return newMultiPdfEngines(
|
return newMultiPdfEngines(
|
||||||
engines(mod.mergeNames),
|
engines(mod.mergeNames),
|
||||||
engines(mod.splitNames),
|
engines(mod.splitNames),
|
||||||
|
engines(mod.flattenNames),
|
||||||
engines(mod.convertNames),
|
engines(mod.convertNames),
|
||||||
engines(mod.readMetadataNames),
|
engines(mod.readMetadataNames),
|
||||||
engines(mod.writeMetadataNames),
|
engines(mod.writeMetadataNames),
|
||||||
@@ -234,6 +245,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
|||||||
return []api.Route{
|
return []api.Route{
|
||||||
mergeRoute(engine),
|
mergeRoute(engine),
|
||||||
splitRoute(engine),
|
splitRoute(engine),
|
||||||
|
flattenRoute(engine),
|
||||||
convertRoute(engine),
|
convertRoute(engine),
|
||||||
readMetadataRoute(engine),
|
readMetadataRoute(engine),
|
||||||
writeMetadataRoute(engine),
|
writeMetadataRoute(engine),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
ctx *gotenberg.Context
|
ctx *gotenberg.Context
|
||||||
expectedMergePdfEngines []string
|
expectedMergePdfEngines []string
|
||||||
expectedSplitPdfEngines []string
|
expectedSplitPdfEngines []string
|
||||||
|
expectedFlattenPdfEngines []string
|
||||||
expectedConvertPdfEngines []string
|
expectedConvertPdfEngines []string
|
||||||
expectedReadMetadataPdfEngines []string
|
expectedReadMetadataPdfEngines []string
|
||||||
expectedWriteMetadataPdfEngines []string
|
expectedWriteMetadataPdfEngines []string
|
||||||
@@ -68,6 +69,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
}(),
|
}(),
|
||||||
expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"},
|
expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"},
|
||||||
expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"},
|
expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"},
|
||||||
|
expectedFlattenPdfEngines: []string{"qpdf"},
|
||||||
expectedConvertPdfEngines: []string{"libreoffice-pdfengine"},
|
expectedConvertPdfEngines: []string{"libreoffice-pdfengine"},
|
||||||
expectedReadMetadataPdfEngines: []string{"exiftool"},
|
expectedReadMetadataPdfEngines: []string{"exiftool"},
|
||||||
expectedWriteMetadataPdfEngines: []string{"exiftool"},
|
expectedWriteMetadataPdfEngines: []string{"exiftool"},
|
||||||
@@ -109,7 +111,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs := new(PdfEngines).Descriptor().FlagSet
|
fs := new(PdfEngines).Descriptor().FlagSet
|
||||||
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-split-engines=a", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-split-engines=a", "--pdfengines-flatten-engines=c", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -128,6 +130,7 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
|
|
||||||
expectedMergePdfEngines: []string{"b"},
|
expectedMergePdfEngines: []string{"b"},
|
||||||
expectedSplitPdfEngines: []string{"a"},
|
expectedSplitPdfEngines: []string{"a"},
|
||||||
|
expectedFlattenPdfEngines: []string{"c"},
|
||||||
expectedConvertPdfEngines: []string{"b"},
|
expectedConvertPdfEngines: []string{"b"},
|
||||||
expectedReadMetadataPdfEngines: []string{"a"},
|
expectedReadMetadataPdfEngines: []string{"a"},
|
||||||
expectedWriteMetadataPdfEngines: []string{"a"},
|
expectedWriteMetadataPdfEngines: []string{"a"},
|
||||||
@@ -185,6 +188,10 @@ func TestPdfEngines_Provision(t *testing.T) {
|
|||||||
t.Fatalf("expected %d merge names but got %d", len(tc.expectedMergePdfEngines), len(mod.mergeNames))
|
t.Fatalf("expected %d merge names but got %d", len(tc.expectedMergePdfEngines), len(mod.mergeNames))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(tc.expectedFlattenPdfEngines) != len(mod.flattenNames) {
|
||||||
|
t.Fatalf("expected %d flatten names but got %d", len(tc.expectedFlattenPdfEngines), len(mod.flattenNames))
|
||||||
|
}
|
||||||
|
|
||||||
if len(tc.expectedConvertPdfEngines) != len(mod.convertNames) {
|
if len(tc.expectedConvertPdfEngines) != len(mod.convertNames) {
|
||||||
t.Fatalf("expected %d convert names but got %d", len(tc.expectedConvertPdfEngines), len(mod.convertNames))
|
t.Fatalf("expected %d convert names but got %d", len(tc.expectedConvertPdfEngines), len(mod.convertNames))
|
||||||
}
|
}
|
||||||
@@ -317,14 +324,16 @@ func TestPdfEngines_SystemMessages(t *testing.T) {
|
|||||||
mod.readMetadataNames = []string{"foo", "bar"}
|
mod.readMetadataNames = []string{"foo", "bar"}
|
||||||
mod.writeMetadataNames = []string{"foo", "bar"}
|
mod.writeMetadataNames = []string{"foo", "bar"}
|
||||||
|
|
||||||
|
expectedMessages := 6
|
||||||
messages := mod.SystemMessages()
|
messages := mod.SystemMessages()
|
||||||
if len(messages) != 5 {
|
if len(messages) != expectedMessages {
|
||||||
t.Errorf("expected one and only one message, but got %d", len(messages))
|
t.Errorf("expected %d message(s), but got %d", expectedMessages, len(messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
expect := []string{
|
expect := []string{
|
||||||
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames[:], " ")),
|
||||||
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames[:], " ")),
|
||||||
|
fmt.Sprintf("flatten engines - %s", strings.Join(mod.flattenNames[:], " ")),
|
||||||
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")),
|
||||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||||
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
||||||
@@ -382,7 +391,7 @@ func TestPdfEngines_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 5,
|
expectRoutes: 6,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -202,6 +202,21 @@ func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.S
|
|||||||
return outputPaths, nil
|
return outputPaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FlattenStub merges annotation appearances with page content for each given PDF
|
||||||
|
// in the input paths, effectively deleting the original annotations. It generates
|
||||||
|
// new output paths for the flattened PDFs and returns them. If an error occurs
|
||||||
|
// during the flattening process, it returns the error.
|
||||||
|
func FlattenStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string) error {
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
err := engine.Flatten(ctx, ctx.Log(), inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("flatten '%s': %w", inputPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ConvertStub transforms a given PDF to the specified formats defined in
|
// ConvertStub transforms a given PDF to the specified formats defined in
|
||||||
// [gotenberg.PdfFormats]. If no format, it does nothing and returns the input
|
// [gotenberg.PdfFormats]. If no format, it does nothing and returns the input
|
||||||
// paths.
|
// paths.
|
||||||
@@ -255,8 +270,10 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
metadata := FormDataPdfMetadata(form, false)
|
metadata := FormDataPdfMetadata(form, false)
|
||||||
|
|
||||||
var inputPaths []string
|
var inputPaths []string
|
||||||
|
var flatten bool
|
||||||
err := form.
|
err := form.
|
||||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
Bool("flatten", &flatten, false).
|
||||||
Validate()
|
Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
@@ -278,6 +295,13 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("write metadata: %w", err)
|
return fmt.Errorf("write metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flatten {
|
||||||
|
err = FlattenStub(ctx, engine, outputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("flatten PDFs: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = ctx.AddOutputPaths(outputPaths...)
|
err = ctx.AddOutputPaths(outputPaths...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("add output paths: %w", err)
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
@@ -347,6 +371,40 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flattenRoute returns an [api.Route] which can flatten PDFs.
|
||||||
|
func flattenRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/flatten",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
form := ctx.FormData()
|
||||||
|
|
||||||
|
var inputPaths []string
|
||||||
|
err := form.
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = FlattenStub(ctx, engine, inputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("convert PDFs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.AddOutputPaths(inputPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
// convertRoute returns an [api.Route] which can convert PDFs to a specific ODF
|
||||||
// format.
|
// format.
|
||||||
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
func convertRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
|||||||
@@ -719,6 +719,39 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
scenario: "PDF engine flatten error",
|
||||||
|
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{
|
||||||
|
"metadata": {
|
||||||
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
|
},
|
||||||
|
"flatten": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
scenario: "cannot add output paths",
|
scenario: "cannot add output paths",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -754,6 +787,9 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
"{\"Creator\": \"foo\", \"Producer\": \"bar\" }",
|
||||||
},
|
},
|
||||||
|
"flatten": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
@@ -767,6 +803,9 @@ func TestMergeHandler(t *testing.T) {
|
|||||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectHttpError: false,
|
expectHttpError: false,
|
||||||
@@ -1055,6 +1094,147 @@ func TestSplitHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFlattenHandler(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx *api.ContextMock
|
||||||
|
engine gotenberg.PdfEngine
|
||||||
|
expectError bool
|
||||||
|
expectHttpError bool
|
||||||
|
expectHttpStatus int
|
||||||
|
expectOutputPathsCount int
|
||||||
|
expectOutputPaths []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "missing at least one mandatory file",
|
||||||
|
ctx: &api.ContextMock{Context: new(api.Context)},
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return errors.New("foo")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "cannot add output paths",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
ctx.SetCancelled(true)
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: true,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success with single file",
|
||||||
|
ctx: func() *api.ContextMock {
|
||||||
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
|
ctx.SetFiles(map[string]string{
|
||||||
|
"file.pdf": "/file.pdf",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success (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",
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}(),
|
||||||
|
engine: &gotenberg.PdfEngineMock{
|
||||||
|
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
expectHttpError: false,
|
||||||
|
expectOutputPathsCount: 2,
|
||||||
|
expectOutputPaths: []string{"/file.pdf", "/file2.pdf"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
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 := flattenRoute(tc.engine).Handler(c)
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var httpErr api.HttpError
|
||||||
|
isHttpError := errors.As(err, &httpErr)
|
||||||
|
|
||||||
|
if tc.expectHttpError && !isHttpError {
|
||||||
|
t.Errorf("expected an HTTP error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectHttpError && isHttpError {
|
||||||
|
t.Errorf("expected no HTTP error but got one: %v", httpErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && tc.expectHttpError && isHttpError {
|
||||||
|
status, _ := httpErr.HttpError()
|
||||||
|
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 but got %d", tc.expectOutputPathsCount, len(tc.ctx.OutputPaths()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range tc.expectOutputPaths {
|
||||||
|
if !slices.Contains(tc.ctx.OutputPaths(), path) {
|
||||||
|
t.Errorf("expected '%s' in output paths %v", path, tc.ctx.OutputPaths())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestConvertHandler(t *testing.T) {
|
func TestConvertHandler(t *testing.T) {
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
scenario string
|
scenario string
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths [
|
|||||||
return fmt.Errorf("merge PDFs with PDFtk: %w", err)
|
return fmt.Errorf("merge PDFs with PDFtk: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten is not available in this implementation.
|
||||||
|
func (engine *PdfTk) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
return fmt.Errorf("flatten PDF with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert is not available in this implementation.
|
// Convert is not available in this implementation.
|
||||||
func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with PDFtk: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -232,6 +232,15 @@ func TestPdfCpu_Split(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPdfTk_Flatten(t *testing.T) {
|
||||||
|
engine := new(PdfTk)
|
||||||
|
err := engine.Flatten(context.TODO(), zap.NewNop(), "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPdfEngineMethodNotSupported) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPdfEngineMethodNotSupported, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPdfTk_Convert(t *testing.T) {
|
func TestPdfTk_Convert(t *testing.T) {
|
||||||
engine := new(PdfTk)
|
engine := new(PdfTk)
|
||||||
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|||||||
@@ -101,6 +101,27 @@ func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths []
|
|||||||
return fmt.Errorf("merge PDFs with QPDF: %w", err)
|
return fmt.Errorf("merge PDFs with QPDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flatten merges annotation appearances with page content, deleting the original annotations.
|
||||||
|
func (engine *QPdf) Flatten(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||||
|
var args []string
|
||||||
|
args = append(args, "--generate-appearances")
|
||||||
|
args = append(args, "--flatten-annotations=all")
|
||||||
|
args = append(args, "--replace-input")
|
||||||
|
args = append(args, inputPath)
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cmd.Exec()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("flatten PDFs with QPDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Convert is not available in this implementation.
|
// Convert is not available in this implementation.
|
||||||
func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||||
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("convert PDF to '%+v' with QPDF: %w", formats, gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package qpdf
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -232,6 +234,101 @@ func TestQPdf_Split(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQPdf_Flatten(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
ctx context.Context
|
||||||
|
inputPath string
|
||||||
|
createCopy bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "invalid context",
|
||||||
|
ctx: nil,
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "invalid input path",
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPath: "foo.pdf",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "success",
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPath: "/tests/test/testdata/pdfengines/sample3.pdf",
|
||||||
|
createCopy: true,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
engine := new(QPdf)
|
||||||
|
err := engine.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var destinationPath string
|
||||||
|
if tc.createCopy {
|
||||||
|
fs := gotenberg.NewFileSystem(new(gotenberg.OsMkdirAll))
|
||||||
|
outputDir, err := fs.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected error no but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = os.RemoveAll(fs.WorkingDirPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error while cleaning up but got: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
destinationPath = fmt.Sprintf("%s/copy_temp.pdf", outputDir)
|
||||||
|
source, err := os.Open(tc.inputPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open source file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(source *os.File) {
|
||||||
|
err := source.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}(source)
|
||||||
|
|
||||||
|
destination, err := os.Create(destinationPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create destination file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(destination *os.File) {
|
||||||
|
err := destination.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("close file: %v", err)
|
||||||
|
}
|
||||||
|
}(destination)
|
||||||
|
|
||||||
|
_, err = io.Copy(destination, source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy source into destination: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
destinationPath = tc.inputPath
|
||||||
|
}
|
||||||
|
|
||||||
|
err = engine.Flatten(tc.ctx, zap.NewNop(), destinationPath)
|
||||||
|
|
||||||
|
if !tc.expectError && err != nil {
|
||||||
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.expectError && err == nil {
|
||||||
|
t.Fatal("expected error but got none")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestQPdf_Convert(t *testing.T) {
|
func TestQPdf_Convert(t *testing.T) {
|
||||||
engine := new(QPdf)
|
engine := new(QPdf)
|
||||||
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
err := engine.Convert(context.TODO(), zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||||
|
|||||||
BIN
test/testdata/pdfengines/sample3.pdf
vendored
Normal file
BIN
test/testdata/pdfengines/sample3.pdf
vendored
Normal file
Binary file not shown.
Reference in New Issue
Block a user