From 2598387b21b6e8e189f695e09ffe7d1133da182c Mon Sep 17 00:00:00 2001 From: Peter Chakalov Date: Wed, 22 Jan 2025 17:11:08 +0200 Subject: [PATCH] initial changes --- Makefile | 2 + pkg/gotenberg/mocks.go | 5 ++ pkg/gotenberg/pdfengine.go | 4 ++ pkg/modules/exiftool/exiftool.go | 5 ++ .../libreoffice/pdfengine/pdfengine.go | 5 ++ pkg/modules/pdfcpu/pdfcpu.go | 5 ++ pkg/modules/pdfengines/multi.go | 26 ++++++++ pkg/modules/pdfengines/pdfengines.go | 9 +++ pkg/modules/pdfengines/pdfengines_test.go | 11 +++- pkg/modules/pdfengines/routes.go | 59 +++++++++++++++++++ pkg/modules/pdftk/pdftk.go | 5 ++ pkg/modules/qpdf/qpdf.go | 5 ++ 12 files changed, 139 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index de2e0dbd..904baea3 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,7 @@ LOG_FIELDS_PREFIX= PDFENGINES_ENGINES= PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk +PDFENGINES_FLATTEN_ENGINES=qpdf PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine PDFENGINES_READ_METADATA_ENGINES=exiftool PDFENGINES_WRITE_METADATA_ENGINES=exiftool @@ -143,6 +144,7 @@ run: ## Start a Gotenberg container --pdfengines-engines=$(PDFENGINES_ENGINES) \ --pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \ --pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \ + --pdfengines-convert-engines=$(PDFENGINES_FLATTEN_ENGINES) \ --pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \ --pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \ --pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \ diff --git a/pkg/gotenberg/mocks.go b/pkg/gotenberg/mocks.go index 2ade8952..65e398ea 100644 --- a/pkg/gotenberg/mocks.go +++ b/pkg/gotenberg/mocks.go @@ -38,6 +38,7 @@ func (mod *ValidatorMock) Validate() error { type PdfEngineMock struct { 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) + FlattenMock func(ctx context.Context, logger *zap.Logger, 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) WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error @@ -51,6 +52,10 @@ func (engine *PdfEngineMock) Split(ctx context.Context, logger *zap.Logger, mode return engine.SplitMock(ctx, logger, mode, inputPath, outputDirPath) } +func (engine *PdfEngineMock) Flatten(ctx context.Context, logger *zap.Logger, inputPath, outputPath string) error { + return engine.FlattenMock(ctx, logger, 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) } diff --git a/pkg/gotenberg/pdfengine.go b/pkg/gotenberg/pdfengine.go index 788c07c7..a59efccf 100644 --- a/pkg/gotenberg/pdfengine.go +++ b/pkg/gotenberg/pdfengine.go @@ -96,6 +96,10 @@ type PdfEngine interface { // Split splits a given PDF file. Split(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error) + // Flatten PDF refers to merging the annotations (such as markup, widgets, 3D models, etc.) into a static area that is part of the PDF document + // By flattening and merging existing annotation appearances with page content, the original annotations are deleted from the PDF pages. + Flatten(ctx context.Context, logger *zap.Logger, 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 diff --git a/pkg/modules/exiftool/exiftool.go b/pkg/modules/exiftool/exiftool.go index aeffc6a9..538fbd87 100644 --- a/pkg/modules/exiftool/exiftool.go +++ b/pkg/modules/exiftool/exiftool.go @@ -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) } +// Flatten is not available in this implementation. +func (engine *ExifTool) Flatten(ctx context.Context, logger *zap.Logger, inputPath, outputPath string) error { + return fmt.Errorf("flatten PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // Convert is not available in this implementation. 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) diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine.go b/pkg/modules/libreoffice/pdfengine/pdfengine.go index 5416ab35..9f38d11d 100644 --- a/pkg/modules/libreoffice/pdfengine/pdfengine.go +++ b/pkg/modules/libreoffice/pdfengine/pdfengine.go @@ -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) } +// Flatten is not available in this implementation. +func (engine *LibreOfficePdfEngine) Flatten(ctx context.Context, logger *zap.Logger, inputPath, outputPath 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 // 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] diff --git a/pkg/modules/pdfcpu/pdfcpu.go b/pkg/modules/pdfcpu/pdfcpu.go index 29803cb3..aceacdff 100644 --- a/pkg/modules/pdfcpu/pdfcpu.go +++ b/pkg/modules/pdfcpu/pdfcpu.go @@ -107,6 +107,11 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb return outputPaths, nil } +// Flatten is not available in this implementation. +func (engine *PdfCpu) Flatten(ctx context.Context, logger *zap.Logger, inputPath, outputPath string) error { + return fmt.Errorf("flatten PDF with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // 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) diff --git a/pkg/modules/pdfengines/multi.go b/pkg/modules/pdfengines/multi.go index a01515b5..e3d4c456 100644 --- a/pkg/modules/pdfengines/multi.go +++ b/pkg/modules/pdfengines/multi.go @@ -14,6 +14,7 @@ import ( type multiPdfEngines struct { mergeEngines []gotenberg.PdfEngine splitEngines []gotenberg.PdfEngine + flattenEngines []gotenberg.PdfEngine convertEngines []gotenberg.PdfEngine readMetadataEngines []gotenberg.PdfEngine writeMetadataEngines []gotenberg.PdfEngine @@ -22,6 +23,7 @@ type multiPdfEngines struct { func newMultiPdfEngines( mergeEngines, splitEngines, + flattenEngines, convertEngines, readMetadataEngines, writeMetadataEngines []gotenberg.PdfEngine, @@ -29,6 +31,7 @@ func newMultiPdfEngines( return &multiPdfEngines{ mergeEngines: mergeEngines, splitEngines: splitEngines, + flattenEngines: flattenEngines, convertEngines: convertEngines, readMetadataEngines: readMetadataEngines, writeMetadataEngines: writeMetadataEngines, @@ -98,6 +101,29 @@ func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.Logger, mod return nil, fmt.Errorf("split PDF with multi PDF engines: %w", err) } +func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap.Logger, inputPath, outputPath 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, outputPath) + }(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 // 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 { diff --git a/pkg/modules/pdfengines/pdfengines.go b/pkg/modules/pdfengines/pdfengines.go index 4536ff7b..b7cd4609 100644 --- a/pkg/modules/pdfengines/pdfengines.go +++ b/pkg/modules/pdfengines/pdfengines.go @@ -29,6 +29,7 @@ func init() { type PdfEngines struct { mergeNames []string splitNames []string + flattenNames []string convertNames []string readMetadataNames []string writeMetadataNames []string @@ -44,6 +45,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor { 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-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-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") @@ -67,6 +69,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error { flags := ctx.ParsedFlags() mergeNames := flags.MustStringSlice("pdfengines-merge-engines") splitNames := flags.MustStringSlice("pdfengines-split-engines") + flattenNames := flags.MustStringSlice("pdfengines-flatten-engines") convertNames := flags.MustStringSlice("pdfengines-convert-engines") readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines") writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines") @@ -106,6 +109,10 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error { mod.splitNames = splitNames } + if len(flattenNames) > 0 { + mod.flattenNames = flattenNames + } + mod.convertNames = defaultNames if len(convertNames) > 0 { mod.convertNames = convertNames @@ -212,6 +219,7 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) { return newMultiPdfEngines( engines(mod.mergeNames), engines(mod.splitNames), + engines(mod.flattenNames), engines(mod.convertNames), engines(mod.readMetadataNames), engines(mod.writeMetadataNames), @@ -234,6 +242,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) { return []api.Route{ mergeRoute(engine), splitRoute(engine), + flattenRoute(engine), convertRoute(engine), readMetadataRoute(engine), writeMetadataRoute(engine), diff --git a/pkg/modules/pdfengines/pdfengines_test.go b/pkg/modules/pdfengines/pdfengines_test.go index 66546ebe..c4e07eff 100644 --- a/pkg/modules/pdfengines/pdfengines_test.go +++ b/pkg/modules/pdfengines/pdfengines_test.go @@ -27,6 +27,7 @@ func TestPdfEngines_Provision(t *testing.T) { ctx *gotenberg.Context expectedMergePdfEngines []string expectedSplitPdfEngines []string + expectedFlattenPdfEngines []string expectedConvertPdfEngines []string expectedReadMetadataPdfEngines []string expectedWriteMetadataPdfEngines []string @@ -68,6 +69,7 @@ func TestPdfEngines_Provision(t *testing.T) { }(), expectedMergePdfEngines: []string{"qpdf", "pdfcpu", "pdftk"}, expectedSplitPdfEngines: []string{"pdfcpu", "qpdf", "pdftk"}, + expectedFlattenPdfEngines: []string{"qpdf"}, expectedConvertPdfEngines: []string{"libreoffice-pdfengine"}, expectedReadMetadataPdfEngines: []string{"exiftool"}, expectedWriteMetadataPdfEngines: []string{"exiftool"}, @@ -109,7 +111,7 @@ func TestPdfEngines_Provision(t *testing.T) { } 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 { t.Fatalf("expected no error but got: %v", err) } @@ -128,6 +130,7 @@ func TestPdfEngines_Provision(t *testing.T) { expectedMergePdfEngines: []string{"b"}, expectedSplitPdfEngines: []string{"a"}, + expectedFlattenPdfEngines: []string{"c"}, expectedConvertPdfEngines: []string{"b"}, expectedReadMetadataPdfEngines: []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)) } + 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) { t.Fatalf("expected %d convert names but got %d", len(tc.expectedConvertPdfEngines), len(mod.convertNames)) } @@ -382,7 +389,7 @@ func TestPdfEngines_Routes(t *testing.T) { }{ { scenario: "routes not disabled", - expectRoutes: 5, + expectRoutes: 6, disableRoutes: false, }, { diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index d164bc77..d6285e76 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -202,6 +202,20 @@ func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.S return outputPaths, nil } +func FlattenStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string) ([]string, error) { + outputPaths := make([]string, len(inputPaths)) + for i, inputPath := range inputPaths { + outputPaths[i] = ctx.GeneratePath(".pdf") + + err := engine.Flatten(ctx, ctx.Log(), inputPath, outputPaths[i]) + if err != nil { + return nil, fmt.Errorf("flatten '%s': %w", inputPath, err) + } + } + + return outputPaths, nil +} + // ConvertStub transforms a given PDF to the specified formats defined in // [gotenberg.PdfFormats]. If no format, it does nothing and returns the input // paths. @@ -347,6 +361,51 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route { } } +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) + } + + outputPaths, err := FlattenStub(ctx, engine, inputPaths) + if err != nil { + return fmt.Errorf("convert PDFs: %w", err) + } + + if len(outputPaths) > 1 { + // If .zip archive, keep the original filename. + for i, inputPath := range inputPaths { + err = ctx.Rename(outputPaths[i], inputPath) + if err != nil { + return fmt.Errorf("rename output path: %w", err) + } + + outputPaths[i] = inputPath + } + } + + err = ctx.AddOutputPaths(outputPaths...) + 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 // format. func convertRoute(engine gotenberg.PdfEngine) api.Route { diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index c3f63d17..6fb3322a 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -99,6 +99,11 @@ func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, inputPaths [ 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, outputPath string) error { + return fmt.Errorf("flatten PDF with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // 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) diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index 34785ade..898a919b 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -101,6 +101,11 @@ func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inputPaths [] return fmt.Errorf("merge PDFs with QPDF: %w", err) } +// Flatten is not available in this implementation. +func (engine *QPdf) Flatten(ctx context.Context, logger *zap.Logger, inputPath, outputPath string) error { + return fmt.Errorf("flatten PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // 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)