mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 04:32:15 +01:00
feat(split): add splitUnify form field
This commit is contained in:
@@ -43,6 +43,10 @@ type SplitMode struct {
|
|||||||
// Span is either the intervals or the page ranges to extract, depending on
|
// Span is either the intervals or the page ranges to extract, depending on
|
||||||
// the selected mode.
|
// the selected mode.
|
||||||
Span string
|
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 (
|
const (
|
||||||
|
|||||||
@@ -79,8 +79,12 @@ func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, mode gotenb
|
|||||||
case gotenberg.SplitModeIntervals:
|
case gotenberg.SplitModeIntervals:
|
||||||
args = append(args, "split", "-mode", "span", inputPath, outputDirPath, mode.Span)
|
args = append(args, "split", "-mode", "span", inputPath, outputDirPath, mode.Span)
|
||||||
case gotenberg.SplitModePages:
|
case gotenberg.SplitModePages:
|
||||||
outputPath := fmt.Sprintf("%s/%s", outputDirPath, filepath.Base(inputPath))
|
if mode.Unify {
|
||||||
args = append(args, "trim", "-pages", mode.Span, inputPath, outputPath)
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("split PDFs using mode '%s' with pdfcpu: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
return nil, fmt.Errorf("split PDFs using mode '%s' with pdfcpu: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,14 @@ func TestPdfCpu_Split(t *testing.T) {
|
|||||||
expectError: false,
|
expectError: false,
|
||||||
expectOutputPathsCount: 1,
|
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) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
engine := new(PdfCpu)
|
engine := new(PdfCpu)
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ import (
|
|||||||
// FormDataPdfSplitMode creates a [gotenberg.SplitMode] from the form data.
|
// FormDataPdfSplitMode creates a [gotenberg.SplitMode] from the form data.
|
||||||
func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMode {
|
func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMode {
|
||||||
var (
|
var (
|
||||||
mode string
|
mode string
|
||||||
span string
|
span string
|
||||||
|
unify bool
|
||||||
)
|
)
|
||||||
|
|
||||||
splitModeFunc := func(value string) error {
|
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{
|
return gotenberg.SplitMode{
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
Span: span,
|
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)
|
return nil, fmt.Errorf("split PDF '%s': %w", inputPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if mode.Mode == gotenberg.SplitModePages {
|
|
||||||
return paths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep the original filename.
|
// Keep the original filename.
|
||||||
for i, path := range paths {
|
for i, path := range paths {
|
||||||
newPath := fmt.Sprintf(
|
var newPath string
|
||||||
"%s/%s_%d.pdf",
|
if mode.Unify && mode.Mode == gotenberg.SplitModePages {
|
||||||
outputDirPath, filenameNoExt, i,
|
newPath = fmt.Sprintf(
|
||||||
)
|
"%s/%s.pdf",
|
||||||
|
outputDirPath, filenameNoExt,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
newPath = fmt.Sprintf(
|
||||||
|
"%s/%s_%d.pdf",
|
||||||
|
outputDirPath, filenameNoExt, i,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
err = ctx.Rename(path, newPath)
|
err = ctx.Rename(path, newPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -177,6 +192,10 @@ func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode gotenberg.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
outputPaths = append(outputPaths, newPath)
|
outputPaths = append(outputPaths, newPath)
|
||||||
|
|
||||||
|
if mode.Unify && mode.Mode == gotenberg.SplitModePages {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,27 @@ func TestFormDataPdfSplitMode(t *testing.T) {
|
|||||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals},
|
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModeIntervals},
|
||||||
expectValidationError: true,
|
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)",
|
scenario: "valid form fields (intervals)",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
@@ -122,11 +143,14 @@ func TestFormDataPdfSplitMode(t *testing.T) {
|
|||||||
"splitSpan": {
|
"splitSpan": {
|
||||||
"1-2",
|
"1-2",
|
||||||
},
|
},
|
||||||
|
"splitUnify": {
|
||||||
|
"true",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return ctx
|
return ctx
|
||||||
}(),
|
}(),
|
||||||
mandatory: false,
|
mandatory: false,
|
||||||
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
expectedSplitMode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||||
expectValidationError: false,
|
expectValidationError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -442,7 +466,7 @@ func TestSplitPdfStub(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (pages)",
|
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: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
ctx.SetMkdirAll(&gotenberg.MkdirAllMock{MkdirAllMock: func(path string, perm os.FileMode) error {
|
||||||
@@ -940,7 +964,7 @@ func TestSplitHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (intervals)",
|
scenario: "success",
|
||||||
ctx: func() *api.ContextMock {
|
ctx: func() *api.ContextMock {
|
||||||
ctx := &api.ContextMock{Context: new(api.Context)}
|
ctx := &api.ContextMock{Context: new(api.Context)}
|
||||||
ctx.SetFiles(map[string]string{
|
ctx.SetFiles(map[string]string{
|
||||||
@@ -978,45 +1002,6 @@ func TestSplitHandler(t *testing.T) {
|
|||||||
expectOutputPathsCount: 2,
|
expectOutputPathsCount: 2,
|
||||||
expectOutputPaths: []string{"/file/file_0.pdf", "/file/file_1.pdf"},
|
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) {
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
tc.ctx.SetLogger(zap.NewNop())
|
tc.ctx.SetLogger(zap.NewNop())
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ func (engine *PdfTk) Split(ctx context.Context, logger *zap.Logger, mode gotenbe
|
|||||||
|
|
||||||
switch mode.Mode {
|
switch mode.Mode {
|
||||||
case gotenberg.SplitModePages:
|
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)
|
args = append(args, inputPath, "cat", mode.Span, "output", outputPath)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("split PDFs using mode '%s' with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
return nil, fmt.Errorf("split PDFs using mode '%s' with PDFtk: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||||
|
|||||||
@@ -159,25 +159,33 @@ func TestPdfCpu_Split(t *testing.T) {
|
|||||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||||
expectOutputPathsCount: 0,
|
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",
|
scenario: "invalid context",
|
||||||
ctx: nil,
|
ctx: nil,
|
||||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid input path",
|
scenario: "invalid input path",
|
||||||
ctx: context.TODO(),
|
ctx: context.TODO(),
|
||||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||||
inputPath: "",
|
inputPath: "",
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (pages)",
|
scenario: "success (pages & unify)",
|
||||||
ctx: context.TODO(),
|
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",
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ func (engine *QPdf) Split(ctx context.Context, logger *zap.Logger, mode gotenber
|
|||||||
|
|
||||||
switch mode.Mode {
|
switch mode.Mode {
|
||||||
case gotenberg.SplitModePages:
|
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)
|
args = append(args, inputPath, "--pages", ".", mode.Span, "--", outputPath)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("split PDFs using mode '%s' with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
return nil, fmt.Errorf("split PDFs using mode '%s' with QPDF: %w", mode.Mode, gotenberg.ErrPdfSplitModeNotSupported)
|
||||||
|
|||||||
@@ -159,25 +159,33 @@ func TestQPdf_Split(t *testing.T) {
|
|||||||
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
expectedError: gotenberg.ErrPdfSplitModeNotSupported,
|
||||||
expectOutputPathsCount: 0,
|
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",
|
scenario: "invalid context",
|
||||||
ctx: nil,
|
ctx: nil,
|
||||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "invalid input path",
|
scenario: "invalid input path",
|
||||||
ctx: context.TODO(),
|
ctx: context.TODO(),
|
||||||
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2"},
|
mode: gotenberg.SplitMode{Mode: gotenberg.SplitModePages, Span: "1-2", Unify: true},
|
||||||
inputPath: "",
|
inputPath: "",
|
||||||
expectError: true,
|
expectError: true,
|
||||||
expectOutputPathsCount: 0,
|
expectOutputPathsCount: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "success (pages)",
|
scenario: "success (pages & unify)",
|
||||||
ctx: context.TODO(),
|
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",
|
inputPath: "/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
expectError: false,
|
expectError: false,
|
||||||
expectOutputPathsCount: 1,
|
expectOutputPathsCount: 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user