diff --git a/pkg/gotenberg/pdfengine.go b/pkg/gotenberg/pdfengine.go index bc74f09f..788c07c7 100644 --- a/pkg/gotenberg/pdfengine.go +++ b/pkg/gotenberg/pdfengine.go @@ -43,6 +43,10 @@ type SplitMode struct { // Span is either the intervals or the page ranges to extract, depending on // the selected mode. Span string + + // Unify specifies whether to put extracted pages into a single file or as + // many files as there are page ranges. Only works with "pages" mode. + Unify bool } const ( diff --git a/pkg/modules/pdfcpu/pdfcpu.go b/pkg/modules/pdfcpu/pdfcpu.go index b59573c1..29803cb3 100644 --- a/pkg/modules/pdfcpu/pdfcpu.go +++ b/pkg/modules/pdfcpu/pdfcpu.go @@ -79,8 +79,12 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb case gotenberg.SplitModeIntervals: args = append(args, "split", "-mode", "span", inputPath, outputDirPath, mode.Span) case gotenberg.SplitModePages: - outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath)) - args = append(args, "trim", "-pages", mode.Span, inputPath, outputPath) + if mode.Unify { + outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath)) + args = append(args, "trim", "-pages", mode.Span, inputPath, outputPath) + break + } + args = append(args, "extract", "-mode", "page", "-pages", mode.Span, inputPath, outputDirPath) default: return nil, fmt.Errorf("split PDFs using mode '%s' with pdfcpu: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) } diff --git a/pkg/modules/pdfcpu/pdfcpu_test.go b/pkg/modules/pdfcpu/pdfcpu_test.go index e962fc69..e996aed8 100644 --- a/pkg/modules/pdfcpu/pdfcpu_test.go +++ b/pkg/modules/pdfcpu/pdfcpu_test.go @@ -189,6 +189,14 @@ func TestPdfCpu_Split(t *testing.T) { expectError: false, expectOutputPathsCount: 1, }, + { + scenario: "success (pages & unify)", + ctx: context.TODO(), + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, + inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", + expectError: false, + expectOutputPathsCount: 1, + }, } { t.Run(tc.scenario, func(t *testing.T) { engine := new(PdfCpu) diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index 2a76274b..d164bc77 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -18,8 +18,9 @@ import ( // FormDataPdfSplitMode creates a [gotenberg.SplitMode] from the form data. func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMode { var ( - mode string - span string + mode string + span string + unify bool ) splitModeFunc := func(value string) error { @@ -66,9 +67,19 @@ func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMod }) } + form. + Bool("splitUnify", &unify, false). + Custom("splitUnify", func(value string) error { + if value != "" && unify && mode != gotenberg.SplitModePages { + return fmt.Errorf("unify is not available for split mode '%s'", mode) + } + return nil + }) + return gotenberg.SplitMode{ - Mode: mode, - Span: span, + Mode: mode, + Span: span, + Unify: unify, } } @@ -160,16 +171,20 @@ func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.S return nil, fmt.Errorf("split PDF '%s': %w", inputPath, err) } - if mode.Mode == gotenberg.SplitModePages { - return paths, nil - } - // Keep the original filename. for i, path := range paths { - newPath := fmt.Sprintf( - "%s/%s_%d.pdf", - outputDirPath, filenameNoExt, i, - ) + var newPath string + if mode.Unify && mode.Mode == gotenberg.SplitModePages { + newPath = fmt.Sprintf( + "%s/%s.pdf", + outputDirPath, filenameNoExt, + ) + } else { + newPath = fmt.Sprintf( + "%s/%s_%d.pdf", + outputDirPath, filenameNoExt, i, + ) + } err = ctx.Rename(path, newPath) if err != nil { @@ -177,6 +192,10 @@ func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.S } outputPaths = append(outputPaths, newPath) + + if mode.Unify && mode.Mode == gotenberg.SplitModePages { + break + } } } diff --git a/pkg/modules/pdfengines/routes_test.go b/pkg/modules/pdfengines/routes_test.go index 6e0e5e3e..a0b004fb 100644 --- a/pkg/modules/pdfengines/routes_test.go +++ b/pkg/modules/pdfengines/routes_test.go @@ -93,6 +93,27 @@ func TestFormDataPdfSplitMode(t *testing.T) { expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals}, expectValidationError: true, }, + { + scenario: "invalid splitUnify (intervals)", + ctx: func() *api.ContextMock { + ctx := &api.ContextMock{Context: new(api.Context)} + ctx.SetValues(map[string][]string{ + "splitMode": { + "intervals", + }, + "splitSpan": { + "1", + }, + "splitUnify": { + "true", + }, + }) + return ctx + }(), + mandatory: false, + expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals, Span: "1", Unify: true}, + expectValidationError: true, + }, { scenario: "valid form fields (intervals)", ctx: func() *api.ContextMock { @@ -122,11 +143,14 @@ func TestFormDataPdfSplitMode(t *testing.T) { "splitSpan": { "1-2", }, + "splitUnify": { + "true", + }, }) return ctx }(), mandatory: false, - expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, expectValidationError: false, }, { @@ -442,7 +466,7 @@ func TestSplitPdfStub(t *testing.T) { }, { scenario: "success (pages)", - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, ctx: func() *api.ContextMock { ctx := &api.ContextMock{Context: new(api.Context)} ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error { @@ -940,7 +964,7 @@ func TestSplitHandler(t *testing.T) { expectOutputPathsCount: 0, }, { - scenario: "success (intervals)", + scenario: "success", ctx: func() *api.ContextMock { ctx := &api.ContextMock{Context: new(api.Context)} ctx.SetFiles(map[string]string{ @@ -978,45 +1002,6 @@ func TestSplitHandler(t *testing.T) { expectOutputPathsCount: 2, expectOutputPaths: []string{"/file/file_0.pdf", "/file/file_1.pdf"}, }, - { - scenario: "success (pages)", - 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{ - "splitMode": { - gotenberg.SplitModePages, - }, - "splitSpan": { - "1-2", - }, - "pdfua": { - "true", - }, - "metadata": { - "{\"Creator\": \"foo\", \"Producer\": \"bar\" }", - }, - }) - return ctx - }(), - engine: &gotenberg.PdfEngineMock{ - SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) { - return []string{"/file/file.pdf"}, nil - }, - ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error { - return nil - }, - WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error { - return nil - }, - }, - expectError: false, - expectHttpError: false, - expectOutputPathsCount: 1, - expectOutputPaths: []string{"/file/file.pdf"}, - }, } { t.Run(tc.scenario, func(t *testing.T) { tc.ctx.SetLogger(zap.NewNop()) diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index d8870a26..c3f63d17 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -59,6 +59,9 @@ func (engine *PdfTk) Split(ctx context.Context, logger *zap.Logger, mode gotenbe switch mode.Mode { case gotenberg.SplitModePages: + if !mode.Unify { + return nil, fmt.Errorf("split PDFs using mode '%s' without unify with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) + } args = append(args, inputPath, "cat", mode.Span, "output", outputPath) default: return nil, fmt.Errorf("split PDFs using mode '%s' with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) diff --git a/pkg/modules/pdftk/pdftk_test.go b/pkg/modules/pdftk/pdftk_test.go index d5dcd573..73311f72 100644 --- a/pkg/modules/pdftk/pdftk_test.go +++ b/pkg/modules/pdftk/pdftk_test.go @@ -159,25 +159,33 @@ func TestPdfCpu_Split(t *testing.T) { expectedError: gotenberg.ErrPdfSplitModeNotSupported, expectOutputPathsCount: 0, }, + { + scenario: "ErrPdfSplitModeNotSupported (no unify with pages)", + ctx: context.TODO(), + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1", Unify: false}, + expectError: true, + expectedError: gotenberg.ErrPdfSplitModeNotSupported, + expectOutputPathsCount: 0, + }, { scenario: "invalid context", ctx: nil, - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, expectError: true, expectOutputPathsCount: 0, }, { scenario: "invalid input path", ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, inputPath: "", expectError: true, expectOutputPathsCount: 0, }, { - scenario: "success (pages)", + scenario: "success (pages & unify)", ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", expectError: false, expectOutputPathsCount: 1, diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index 2c010d2e..34785ade 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -59,6 +59,9 @@ func (engine *QPdf) Split(ctx context.Context, logger *zap.Logger, mode gotenber switch mode.Mode { case gotenberg.SplitModePages: + if !mode.Unify { + return nil, fmt.Errorf("split PDFs using mode '%s' without unify with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) + } args = append(args, inputPath, "--pages", ".", mode.Span, "--", outputPath) default: return nil, fmt.Errorf("split PDFs using mode '%s' with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported) diff --git a/pkg/modules/qpdf/qpdf_test.go b/pkg/modules/qpdf/qpdf_test.go index b32976cb..9c79721b 100644 --- a/pkg/modules/qpdf/qpdf_test.go +++ b/pkg/modules/qpdf/qpdf_test.go @@ -159,25 +159,33 @@ func TestQPdf_Split(t *testing.T) { expectedError: gotenberg.ErrPdfSplitModeNotSupported, expectOutputPathsCount: 0, }, + { + scenario: "ErrPdfSplitModeNotSupported (no unify with pages)", + ctx: context.TODO(), + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1", Unify: false}, + expectError: true, + expectedError: gotenberg.ErrPdfSplitModeNotSupported, + expectOutputPathsCount: 0, + }, { scenario: "invalid context", ctx: nil, - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, expectError: true, expectOutputPathsCount: 0, }, { scenario: "invalid input path", ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, inputPath: "", expectError: true, expectOutputPathsCount: 0, }, { - scenario: "success (pages)", + scenario: "success (pages & unify)", ctx: context.TODO(), - mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"}, + mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true}, inputPath: "/tests/test/testdata/pdfengines/sample1.pdf", expectError: false, expectOutputPathsCount: 1,