mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 00:22:14 +01:00
feat(pdfengines): add rotate feature
This commit is contained in:
4
Makefile
4
Makefile
@@ -69,6 +69,7 @@ PDFENGINES_WRITE_BOOKMARKS_ENGINES=pdfcpu
|
|||||||
PDFENGINES_WATERMARK_ENGINES=pdfcpu,pdftk
|
PDFENGINES_WATERMARK_ENGINES=pdfcpu,pdftk
|
||||||
PDFENGINES_STAMP_ENGINES=pdfcpu,pdftk
|
PDFENGINES_STAMP_ENGINES=pdfcpu,pdftk
|
||||||
PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk
|
PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk
|
||||||
|
PDFENGINES_ROTATE_ENGINES=pdfcpu,pdftk
|
||||||
PDFENGINES_EMBED_ENGINES=pdfcpu
|
PDFENGINES_EMBED_ENGINES=pdfcpu
|
||||||
PROMETHEUS_NAMESPACE=gotenberg
|
PROMETHEUS_NAMESPACE=gotenberg
|
||||||
PROMETHEUS_COLLECT_INTERVAL=1s
|
PROMETHEUS_COLLECT_INTERVAL=1s
|
||||||
@@ -151,6 +152,7 @@ run: ## Start a Gotenberg container
|
|||||||
--pdfengines-watermark-engines=$(PDFENGINES_WATERMARK_ENGINES) \
|
--pdfengines-watermark-engines=$(PDFENGINES_WATERMARK_ENGINES) \
|
||||||
--pdfengines-stamp-engines=$(PDFENGINES_STAMP_ENGINES) \
|
--pdfengines-stamp-engines=$(PDFENGINES_STAMP_ENGINES) \
|
||||||
--pdfengines-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \
|
--pdfengines-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \
|
||||||
|
--pdfengines-rotate-engines=$(PDFENGINES_ROTATE_ENGINES) \
|
||||||
--pdfengines-embed-engines=$(PDFENGINES_EMBED_ENGINES) \
|
--pdfengines-embed-engines=$(PDFENGINES_EMBED_ENGINES) \
|
||||||
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
||||||
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||||
@@ -203,6 +205,8 @@ NO_CONCURRENCY=false
|
|||||||
# watermark
|
# watermark
|
||||||
# pdfengines-stamp
|
# pdfengines-stamp
|
||||||
# stamp
|
# stamp
|
||||||
|
# pdfengines-rotate
|
||||||
|
# rotate
|
||||||
# pdfengines-bookmarks
|
# pdfengines-bookmarks
|
||||||
# bookmarks
|
# bookmarks
|
||||||
# prometheus-metrics
|
# prometheus-metrics
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type PdfEngineMock struct {
|
|||||||
WriteBookmarksMock func(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []Bookmark) error
|
WriteBookmarksMock func(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []Bookmark) error
|
||||||
WatermarkMock func(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
|
WatermarkMock func(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
|
||||||
StampMock func(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
|
StampMock func(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
|
||||||
|
RotateMock func(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
@@ -113,6 +114,10 @@ func (engine *PdfEngineMock) Stamp(ctx context.Context, logger *zap.Logger, inpu
|
|||||||
return engine.StampMock(ctx, logger, inputPath, stamp)
|
return engine.StampMock(ctx, logger, inputPath, stamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (engine *PdfEngineMock) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
return engine.RotateMock(ctx, logger, inputPath, angle, pages)
|
||||||
|
}
|
||||||
|
|
||||||
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
||||||
type PdfEngineProviderMock struct {
|
type PdfEngineProviderMock struct {
|
||||||
PdfEngineMock func() (PdfEngine, error)
|
PdfEngineMock func() (PdfEngine, error)
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ var (
|
|||||||
// ErrPdfStampSourceNotSupported is returned when a stamp source type
|
// ErrPdfStampSourceNotSupported is returned when a stamp source type
|
||||||
// is not supported by the PDF engine.
|
// is not supported by the PDF engine.
|
||||||
ErrPdfStampSourceNotSupported = errors.New("stamp source not supported")
|
ErrPdfStampSourceNotSupported = errors.New("stamp source not supported")
|
||||||
|
|
||||||
|
// ErrPdfRotateAngleNotSupported is returned when the rotation angle is
|
||||||
|
// not supported.
|
||||||
|
ErrPdfRotateAngleNotSupported = errors.New("rotation angle not supported")
|
||||||
)
|
)
|
||||||
|
|
||||||
// PdfEngineInvalidArgsError represents an error returned by a PDF engine when
|
// PdfEngineInvalidArgsError represents an error returned by a PDF engine when
|
||||||
@@ -203,6 +207,10 @@ type PdfEngine interface {
|
|||||||
|
|
||||||
// Stamp applies a stamp (on top of page content) to a PDF file.
|
// Stamp applies a stamp (on top of page content) to a PDF file.
|
||||||
Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
|
Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
|
||||||
|
|
||||||
|
// Rotate rotates pages of a PDF file by the given angle (90, 180, 270).
|
||||||
|
// If pages is empty, all pages are rotated.
|
||||||
|
Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
||||||
|
|||||||
@@ -65,6 +65,10 @@ func ParseError(err error) (int, string) {
|
|||||||
return http.StatusBadRequest, "At least one PDF engine cannot process the requested stamp source type, while others may have failed due to different issues"
|
return http.StatusBadRequest, "At least one PDF engine cannot process the requested stamp source type, while others may have failed due to different issues"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, gotenberg.ErrPdfRotateAngleNotSupported) {
|
||||||
|
return http.StatusBadRequest, "At least one PDF engine cannot process the requested rotation angle, while others may have failed due to different issues"
|
||||||
|
}
|
||||||
|
|
||||||
var invalidArgsError *gotenberg.PdfEngineInvalidArgsError
|
var invalidArgsError *gotenberg.PdfEngineInvalidArgsError
|
||||||
if errors.As(err, &invalidArgsError) {
|
if errors.As(err, &invalidArgsError) {
|
||||||
return http.StatusBadRequest, invalidArgsError.Error()
|
return http.StatusBadRequest, invalidArgsError.Error()
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
||||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||||
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
||||||
|
rotateAngle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||||
|
|
||||||
var url string
|
var url string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -435,7 +436,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
stamp.Expression = stampFiles[0]
|
stamp.Expression = stampFiles[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp)
|
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp, rotateAngle, rotatePages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -493,6 +494,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
||||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||||
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
||||||
|
rotateAngle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||||
|
|
||||||
var inputPath string
|
var inputPath string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -510,7 +512,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("file://%s", inputPath)
|
url := fmt.Sprintf("file://%s", inputPath)
|
||||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp)
|
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp, rotateAngle, rotatePages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -569,6 +571,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
||||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||||
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
||||||
|
rotateAngle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
inputPath string
|
inputPath string
|
||||||
@@ -595,7 +598,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp)
|
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp, rotateAngle, rotatePages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||||
}
|
}
|
||||||
@@ -719,7 +722,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
|||||||
return fmt.Sprintf("file://%s", inputPath), nil
|
return fmt.Sprintf("file://%s", inputPath), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]any, userPassword, ownerPassword string, embedPaths []string, watermark, stamp gotenberg.Stamp) error {
|
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, options PdfOptions, mode gotenberg.SplitMode, pdfFormats gotenberg.PdfFormats, metadata map[string]any, userPassword, ownerPassword string, embedPaths []string, watermark, stamp gotenberg.Stamp, rotateAngle int, rotatePages string) error {
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/1130.
|
// See https://github.com/gotenberg/gotenberg/issues/1130.
|
||||||
filename := ctx.OutputFilename(outputPath)
|
filename := ctx.OutputFilename(outputPath)
|
||||||
@@ -801,6 +804,11 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
return fmt.Errorf("stamp PDFs: %w", err)
|
return fmt.Errorf("stamp PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = pdfengines.RotateStub(ctx, engine, rotateAngle, rotatePages, outputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rotate PDFs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert PDF(s): %w", err)
|
return fmt.Errorf("convert PDF(s): %w", err)
|
||||||
|
|||||||
@@ -264,6 +264,11 @@ func (engine *ExifTool) Stamp(ctx context.Context, logger *zap.Logger, inputPath
|
|||||||
return fmt.Errorf("stamp PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("stamp PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate is not available in this implementation.
|
||||||
|
func (engine *ExifTool) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
return fmt.Errorf("rotate PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*ExifTool)(nil)
|
_ gotenberg.Module = (*ExifTool)(nil)
|
||||||
|
|||||||
@@ -126,6 +126,11 @@ func (engine *LibreOfficePdfEngine) Stamp(ctx context.Context, logger *zap.Logge
|
|||||||
return fmt.Errorf("stamp PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("stamp PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate is not available in this implementation.
|
||||||
|
func (engine *LibreOfficePdfEngine) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
return fmt.Errorf("rotate PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
|
||||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||||
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
stampFiles := pdfengines.FormDataPdfStampFiles(form)
|
||||||
|
angle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||||
|
|
||||||
zeroValuedSplitMode := gotenberg.SplitMode{}
|
zeroValuedSplitMode := gotenberg.SplitMode{}
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
hasPostProcessing := watermark.Source != "" || stamp.Source != "" ||
|
hasPostProcessing := watermark.Source != "" || stamp.Source != "" || angle != 0 ||
|
||||||
len(embedPaths) > 0 || len(metadata) > 0 || flatten
|
len(embedPaths) > 0 || len(metadata) > 0 || flatten
|
||||||
|
|
||||||
outputPaths := make([]string, len(inputPaths))
|
outputPaths := make([]string, len(inputPaths))
|
||||||
@@ -317,6 +318,11 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
|||||||
return fmt.Errorf("stamp PDFs: %w", err)
|
return fmt.Errorf("stamp PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = pdfengines.RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rotate PDFs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if flatten {
|
if flatten {
|
||||||
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@@ -389,6 +390,27 @@ func (engine *PdfCpu) Stamp(ctx context.Context, logger *zap.Logger, inputPath s
|
|||||||
return engine.applyStampOrWatermark(ctx, logger, "stamp", inputPath, stamp)
|
return engine.applyStampOrWatermark(ctx, logger, "stamp", inputPath, stamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate rotates pages of a PDF file by the given angle using pdfcpu.
|
||||||
|
func (engine *PdfCpu) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
args := []string{"rotate"}
|
||||||
|
if pages != "" {
|
||||||
|
args = append(args, "-pages", pages)
|
||||||
|
}
|
||||||
|
args = append(args, "--", inputPath, strconv.Itoa(angle), 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 fmt.Errorf("rotate PDF with pdfcpu: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (engine *PdfCpu) applyStampOrWatermark(ctx context.Context, logger *zap.Logger, command string, inputPath string, stamp gotenberg.Stamp) error {
|
func (engine *PdfCpu) applyStampOrWatermark(ctx context.Context, logger *zap.Logger, command string, inputPath string, stamp gotenberg.Stamp) error {
|
||||||
var mode string
|
var mode string
|
||||||
switch stamp.Source {
|
switch stamp.Source {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type multiPdfEngines struct {
|
|||||||
writeBookmarksEngines []gotenberg.PdfEngine
|
writeBookmarksEngines []gotenberg.PdfEngine
|
||||||
watermarkEngines []gotenberg.PdfEngine
|
watermarkEngines []gotenberg.PdfEngine
|
||||||
stampEngines []gotenberg.PdfEngine
|
stampEngines []gotenberg.PdfEngine
|
||||||
|
rotateEngines []gotenberg.PdfEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMultiPdfEngines(
|
func newMultiPdfEngines(
|
||||||
@@ -38,7 +39,8 @@ func newMultiPdfEngines(
|
|||||||
readBookmarksEngines,
|
readBookmarksEngines,
|
||||||
writeBookmarksEngines,
|
writeBookmarksEngines,
|
||||||
watermarkEngines,
|
watermarkEngines,
|
||||||
stampEngines []gotenberg.PdfEngine,
|
stampEngines,
|
||||||
|
rotateEngines []gotenberg.PdfEngine,
|
||||||
) *multiPdfEngines {
|
) *multiPdfEngines {
|
||||||
return &multiPdfEngines{
|
return &multiPdfEngines{
|
||||||
mergeEngines: mergeEngines,
|
mergeEngines: mergeEngines,
|
||||||
@@ -53,6 +55,7 @@ func newMultiPdfEngines(
|
|||||||
writeBookmarksEngines: writeBookmarksEngines,
|
writeBookmarksEngines: writeBookmarksEngines,
|
||||||
watermarkEngines: watermarkEngines,
|
watermarkEngines: watermarkEngines,
|
||||||
stampEngines: stampEngines,
|
stampEngines: stampEngines,
|
||||||
|
rotateEngines: rotateEngines,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +428,31 @@ func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *zap.Logger, inp
|
|||||||
return fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
|
return fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate rotates pages of a PDF file using the first available engine that
|
||||||
|
// supports rotation.
|
||||||
|
func (multi *multiPdfEngines) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
var err error
|
||||||
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
|
for _, engine := range multi.rotateEngines {
|
||||||
|
go func(engine gotenberg.PdfEngine) {
|
||||||
|
errChan <- engine.Rotate(ctx, logger, inputPath, angle, pages)
|
||||||
|
}(engine)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case rotateErr := <-errChan:
|
||||||
|
errored := multierr.AppendInto(&err, rotateErr)
|
||||||
|
if !errored {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("rotate PDF with multi PDF engines: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type PdfEngines struct {
|
|||||||
writeBookmarksNames []string
|
writeBookmarksNames []string
|
||||||
watermarkNames []string
|
watermarkNames []string
|
||||||
stampNames []string
|
stampNames []string
|
||||||
|
rotateNames []string
|
||||||
engines []gotenberg.PdfEngine
|
engines []gotenberg.PdfEngine
|
||||||
disableRoutes bool
|
disableRoutes bool
|
||||||
}
|
}
|
||||||
@@ -62,6 +63,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
|||||||
fs.StringSlice("pdfengines-write-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the write bookmarks feature - empty means all")
|
fs.StringSlice("pdfengines-write-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the write bookmarks feature - empty means all")
|
||||||
fs.StringSlice("pdfengines-watermark-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the watermark feature - empty means all")
|
fs.StringSlice("pdfengines-watermark-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the watermark feature - empty means all")
|
||||||
fs.StringSlice("pdfengines-stamp-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the stamp feature - empty means all")
|
fs.StringSlice("pdfengines-stamp-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the stamp feature - empty means all")
|
||||||
|
fs.StringSlice("pdfengines-rotate-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the rotate feature - empty means all")
|
||||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||||
|
|
||||||
// Deprecated flags.
|
// Deprecated flags.
|
||||||
@@ -93,6 +95,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-bookmarks-engines")
|
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-bookmarks-engines")
|
||||||
watermarkNames := flags.MustStringSlice("pdfengines-watermark-engines")
|
watermarkNames := flags.MustStringSlice("pdfengines-watermark-engines")
|
||||||
stampNames := flags.MustStringSlice("pdfengines-stamp-engines")
|
stampNames := flags.MustStringSlice("pdfengines-stamp-engines")
|
||||||
|
rotateNames := flags.MustStringSlice("pdfengines-rotate-engines")
|
||||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||||
|
|
||||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||||
@@ -179,6 +182,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|||||||
mod.stampNames = stampNames
|
mod.stampNames = stampNames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod.rotateNames = defaultNames
|
||||||
|
if len(rotateNames) > 0 {
|
||||||
|
mod.rotateNames = rotateNames
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +240,7 @@ func (mod *PdfEngines) Validate() error {
|
|||||||
findNonExistingEngines(mod.writeBookmarksNames)
|
findNonExistingEngines(mod.writeBookmarksNames)
|
||||||
findNonExistingEngines(mod.watermarkNames)
|
findNonExistingEngines(mod.watermarkNames)
|
||||||
findNonExistingEngines(mod.stampNames)
|
findNonExistingEngines(mod.stampNames)
|
||||||
|
findNonExistingEngines(mod.rotateNames)
|
||||||
|
|
||||||
if len(nonExistingEngines) == 0 {
|
if len(nonExistingEngines) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -256,6 +265,7 @@ func (mod *PdfEngines) SystemMessages() []string {
|
|||||||
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames[:], " ")),
|
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames[:], " ")),
|
||||||
fmt.Sprintf("watermark engines - %s", strings.Join(mod.watermarkNames[:], " ")),
|
fmt.Sprintf("watermark engines - %s", strings.Join(mod.watermarkNames[:], " ")),
|
||||||
fmt.Sprintf("stamp engines - %s", strings.Join(mod.stampNames[:], " ")),
|
fmt.Sprintf("stamp engines - %s", strings.Join(mod.stampNames[:], " ")),
|
||||||
|
fmt.Sprintf("rotate engines - %s", strings.Join(mod.rotateNames[:], " ")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,6 +298,7 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
|||||||
engines(mod.writeBookmarksNames),
|
engines(mod.writeBookmarksNames),
|
||||||
engines(mod.watermarkNames),
|
engines(mod.watermarkNames),
|
||||||
engines(mod.stampNames),
|
engines(mod.stampNames),
|
||||||
|
engines(mod.rotateNames),
|
||||||
), nil
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,6 +328,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
|||||||
embedRoute(engine),
|
embedRoute(engine),
|
||||||
watermarkRoute(engine),
|
watermarkRoute(engine),
|
||||||
stampRoute(engine),
|
stampRoute(engine),
|
||||||
|
rotateRoute(engine),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,56 @@ func FormDataPdfBookmarks(form *api.FormData, mandatory bool) any {
|
|||||||
return bookmarks
|
return bookmarks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormDataPdfRotate creates rotation parameters from the form data.
|
||||||
|
func FormDataPdfRotate(form *api.FormData, mandatory bool) (int, string) {
|
||||||
|
var angle int
|
||||||
|
var pages string
|
||||||
|
|
||||||
|
angleFunc := func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if v != 90 && v != 180 && v != 270 {
|
||||||
|
return errors.New("wrong value, expected 90, 180, or 270")
|
||||||
|
}
|
||||||
|
angle = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mandatory {
|
||||||
|
form.MandatoryCustom("rotateAngle", func(value string) error {
|
||||||
|
return angleFunc(value)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
form.Custom("rotateAngle", func(value string) error {
|
||||||
|
return angleFunc(value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
form.String("rotatePages", &pages, "")
|
||||||
|
|
||||||
|
return angle, pages
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateStub rotates pages of PDF files. If angle is 0, it does nothing.
|
||||||
|
func RotateStub(ctx *api.Context, engine gotenberg.PdfEngine, angle int, pages string, inputPaths []string) error {
|
||||||
|
if angle == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, inputPath := range inputPaths {
|
||||||
|
err := engine.Rotate(ctx, ctx.Log(), inputPath, angle, pages)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rotate '%s': %w", inputPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ValidatePdfFormatsCompat checks for incompatible combinations of PDF formats
|
// ValidatePdfFormatsCompat checks for incompatible combinations of PDF formats
|
||||||
// with other features and returns an appropriate error if found.
|
// with other features and returns an appropriate error if found.
|
||||||
func ValidatePdfFormatsCompat(pdfFormats gotenberg.PdfFormats, userPassword string, embedPaths []string) error {
|
func ValidatePdfFormatsCompat(pdfFormats gotenberg.PdfFormats, userPassword string, embedPaths []string) error {
|
||||||
@@ -561,6 +611,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
||||||
stamp := FormDataPdfStamp(form, false)
|
stamp := FormDataPdfStamp(form, false)
|
||||||
stampFiles := FormDataPdfStampFiles(form)
|
stampFiles := FormDataPdfStampFiles(form)
|
||||||
|
angle, rotatePages := FormDataPdfRotate(form, false)
|
||||||
|
|
||||||
var inputPaths []string
|
var inputPaths []string
|
||||||
var flatten bool
|
var flatten bool
|
||||||
@@ -604,6 +655,11 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("stamp PDFs: %w", err)
|
return fmt.Errorf("stamp PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rotate PDFs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if flatten {
|
if flatten {
|
||||||
err = FlattenStub(ctx, engine, outputPaths)
|
err = FlattenStub(ctx, engine, outputPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -706,6 +762,7 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
||||||
stamp := FormDataPdfStamp(form, false)
|
stamp := FormDataPdfStamp(form, false)
|
||||||
stampFiles := FormDataPdfStampFiles(form)
|
stampFiles := FormDataPdfStampFiles(form)
|
||||||
|
angle, rotatePages := FormDataPdfRotate(form, false)
|
||||||
|
|
||||||
var inputPaths []string
|
var inputPaths []string
|
||||||
var flatten bool
|
var flatten bool
|
||||||
@@ -744,6 +801,11 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("stamp PDFs: %w", err)
|
return fmt.Errorf("stamp PDFs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rotate PDFs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if flatten {
|
if flatten {
|
||||||
err = FlattenStub(ctx, engine, outputPaths)
|
err = FlattenStub(ctx, engine, outputPaths)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1216,3 +1278,38 @@ func stampRoute(engine gotenberg.PdfEngine) api.Route {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rotateRoute returns an [api.Route] which can rotate pages of PDFs.
|
||||||
|
func rotateRoute(engine gotenberg.PdfEngine) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/pdfengines/rotate",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
|
||||||
|
form := ctx.FormData()
|
||||||
|
angle, pages := FormDataPdfRotate(form, true)
|
||||||
|
|
||||||
|
var inputPaths []string
|
||||||
|
err := form.
|
||||||
|
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = RotateStub(ctx, engine, angle, pages, inputPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rotate PDFs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.AddOutputPaths(inputPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add output paths: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -261,6 +261,47 @@ func (engine *PdfTk) Stamp(ctx context.Context, logger *zap.Logger, inputPath st
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate rotates all pages of a PDF file by the given angle using PDFtk.
|
||||||
|
// Page-specific rotation is not supported; if pages is non-empty,
|
||||||
|
// ErrPdfEngineMethodNotSupported is returned.
|
||||||
|
func (engine *PdfTk) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
if pages != "" {
|
||||||
|
return fmt.Errorf("rotate PDF with PDFtk (page-specific rotation): %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
var direction string
|
||||||
|
switch angle {
|
||||||
|
case 90:
|
||||||
|
direction = "east"
|
||||||
|
case 180:
|
||||||
|
direction = "south"
|
||||||
|
case 270:
|
||||||
|
direction = "west"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("rotate PDF with PDFtk: %w", gotenberg.ErrPdfRotateAngleNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpPath := inputPath + ".tmp"
|
||||||
|
args := []string{inputPath, "cat", "1-end" + direction, "output", tmpPath}
|
||||||
|
|
||||||
|
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 fmt.Errorf("rotate PDF with PDFtk: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Rename(tmpPath, inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rename temporary output file with input file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*PdfTk)(nil)
|
_ gotenberg.Module = (*PdfTk)(nil)
|
||||||
|
|||||||
@@ -231,6 +231,11 @@ func (engine *QPdf) Stamp(ctx context.Context, logger *zap.Logger, inputPath str
|
|||||||
return fmt.Errorf("stamp PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
return fmt.Errorf("stamp PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rotate is not available in this implementation.
|
||||||
|
func (engine *QPdf) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||||
|
return fmt.Errorf("rotate PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*QPdf)(nil)
|
_ gotenberg.Module = (*QPdf)(nil)
|
||||||
_ gotenberg.Provisioner = (*QPdf)(nil)
|
_ gotenberg.Provisioner = (*QPdf)(nil)
|
||||||
|
|||||||
@@ -1010,6 +1010,16 @@ Feature: /forms/chromium/convert/html
|
|||||||
Then the response header "Content-Type" should be "application/pdf"
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
Then there should be 1 PDF(s) in the response
|
Then there should be 1 PDF(s) in the response
|
||||||
|
|
||||||
|
@rotate
|
||||||
|
Scenario: POST /forms/chromium/convert/html (Rotate 90)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page-1-html/index.html | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
|
||||||
@embed
|
@embed
|
||||||
Scenario: POST /forms/chromium/convert/html (Embeds)
|
Scenario: POST /forms/chromium/convert/html (Embeds)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
|
|||||||
@@ -585,6 +585,16 @@ Feature: /forms/libreoffice/convert
|
|||||||
Then the response header "Content-Type" should be "application/pdf"
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
Then there should be 1 PDF(s) in the response
|
Then there should be 1 PDF(s) in the response
|
||||||
|
|
||||||
|
@rotate
|
||||||
|
Scenario: POST /forms/libreoffice/convert (Rotate 90)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.docx | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
|
||||||
@watermark
|
@watermark
|
||||||
Scenario: POST /forms/libreoffice/convert (Native Watermark - Text)
|
Scenario: POST /forms/libreoffice/convert (Native Watermark - Text)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
@@ -640,9 +650,10 @@ Feature: /forms/libreoffice/convert
|
|||||||
@metadata
|
@metadata
|
||||||
@watermark
|
@watermark
|
||||||
@stamp
|
@stamp
|
||||||
|
@rotate
|
||||||
@flatten
|
@flatten
|
||||||
@embed
|
@embed
|
||||||
Scenario: POST /forms/libreoffice/convert (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Flatten & Embeds)
|
Scenario: POST /forms/libreoffice/convert (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Rotate & Flatten & Embeds)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
|
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
|
||||||
| files | testdata/page_1.docx | file |
|
| files | testdata/page_1.docx | file |
|
||||||
@@ -653,6 +664,7 @@ Feature: /forms/libreoffice/convert
|
|||||||
| watermarkExpression | CONFIDENTIAL | field |
|
| watermarkExpression | CONFIDENTIAL | field |
|
||||||
| stampSource | text | field |
|
| stampSource | text | field |
|
||||||
| stampExpression | DRAFT | field |
|
| stampExpression | DRAFT | field |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
| flatten | true | field |
|
| flatten | true | field |
|
||||||
| embeds | testdata/embed_1.xml | file |
|
| embeds | testdata/embed_1.xml | file |
|
||||||
| embeds | testdata/embed_2.xml | file |
|
| embeds | testdata/embed_2.xml | file |
|
||||||
|
|||||||
@@ -425,6 +425,17 @@ Feature: /forms/pdfengines/merge
|
|||||||
Then the response header "Content-Type" should be "application/pdf"
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
Then there should be 1 PDF(s) in the response
|
Then there should be 1 PDF(s) in the response
|
||||||
|
|
||||||
|
@rotate
|
||||||
|
Scenario: POST /forms/pdfengines/merge (Rotate 90)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| files | testdata/page_2.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
|
||||||
@embed
|
@embed
|
||||||
Scenario: POST /foo/forms/pdfengines/merge (Embeds)
|
Scenario: POST /foo/forms/pdfengines/merge (Embeds)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
@@ -443,10 +454,11 @@ Feature: /forms/pdfengines/merge
|
|||||||
@metadata
|
@metadata
|
||||||
@watermark
|
@watermark
|
||||||
@stamp
|
@stamp
|
||||||
|
@rotate
|
||||||
@flatten
|
@flatten
|
||||||
@embed
|
@embed
|
||||||
@bookmarks
|
@bookmarks
|
||||||
Scenario: POST /forms/pdfengines/merge (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Flatten & Embeds & Bookmarks)
|
Scenario: POST /forms/pdfengines/merge (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Rotate & Flatten & Embeds & Bookmarks)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
||||||
| files | testdata/page_1.pdf | file |
|
| files | testdata/page_1.pdf | file |
|
||||||
@@ -458,6 +470,7 @@ Feature: /forms/pdfengines/merge
|
|||||||
| watermarkExpression | CONFIDENTIAL | field |
|
| watermarkExpression | CONFIDENTIAL | field |
|
||||||
| stampSource | text | field |
|
| stampSource | text | field |
|
||||||
| stampExpression | DRAFT | field |
|
| stampExpression | DRAFT | field |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
| bookmarks | [{"title":"Merged Index","page":1}] | field |
|
| bookmarks | [{"title":"Merged Index","page":1}] | field |
|
||||||
| flatten | true | field |
|
| flatten | true | field |
|
||||||
| embeds | testdata/embed_1.xml | file |
|
| embeds | testdata/embed_1.xml | file |
|
||||||
|
|||||||
164
test/integration/features/pdfengines_rotate.feature
Normal file
164
test/integration/features/pdfengines_rotate.feature
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
@pdfengines
|
||||||
|
@pdfengines-rotate
|
||||||
|
@rotate
|
||||||
|
Feature: /forms/pdfengines/rotate
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (90 - All Pages - pdfcpu)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/pages_3.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
Then the "pages_3.pdf" PDF should have 3 page(s)
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (180 - All Pages - pdfcpu)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 180 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
Then the "page_1.pdf" PDF should have 1 page(s)
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (270 - All Pages - pdfcpu)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 270 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
Then the "page_1.pdf" PDF should have 1 page(s)
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (90 - Specific Pages - pdfcpu)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/pages_3.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
| rotatePages | 1,3 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
Then the "pages_3.pdf" PDF should have 3 page(s)
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (90 - All Pages - pdftk)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_ROTATE_ENGINES | pdftk |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the response
|
||||||
|
Then the "page_1.pdf" PDF should have 1 page(s)
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Specific Pages - pdftk unsupported)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_ROTATE_ENGINES | pdftk |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/pages_3.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
| rotatePages | 1,3 | field |
|
||||||
|
Then the response status code should be 500
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Bad Request - Invalid Angle)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 45 | field |
|
||||||
|
Then the response status code should be 400
|
||||||
|
Then the response body should contain string:
|
||||||
|
"""
|
||||||
|
Invalid form data: form field 'rotateAngle' is invalid
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Bad Request - Missing Angle)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
Then the response status code should be 400
|
||||||
|
Then the response body should match string:
|
||||||
|
"""
|
||||||
|
Invalid form data: form field 'rotateAngle' is required
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Bad Request - No PDF)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 400
|
||||||
|
Then the response body should match string:
|
||||||
|
"""
|
||||||
|
Invalid form data: no form file found for extensions: [.pdf]
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Many PDFs)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| files | testdata/page_2.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/zip"
|
||||||
|
Then there should be 2 PDF(s) in the response
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Routes Disabled)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| PDFENGINES_DISABLE_ROUTES | true |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 404
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Gotenberg Trace)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
| Gotenberg-Trace | forms_pdfengines_rotate | header |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
|
Then the response header "Gotenberg-Trace" should be "forms_pdfengines_rotate"
|
||||||
|
Then the Gotenberg container should log the following entries:
|
||||||
|
| "trace":"forms_pdfengines_rotate" |
|
||||||
|
|
||||||
|
@webhook
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Webhook)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
Given I have a webhook server
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
| Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header |
|
||||||
|
| Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header |
|
||||||
|
Then the response status code should be 204
|
||||||
|
When I wait for the asynchronous request to the webhook
|
||||||
|
Then the webhook request header "Content-Type" should be "application/pdf"
|
||||||
|
Then there should be 1 PDF(s) in the webhook request
|
||||||
|
|
||||||
|
Scenario: POST /forms/pdfengines/rotate (Basic Auth)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| API_ENABLE_BASIC_AUTH | true |
|
||||||
|
| GOTENBERG_API_BASIC_AUTH_USERNAME | foo |
|
||||||
|
| GOTENBERG_API_BASIC_AUTH_PASSWORD | bar |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 401
|
||||||
|
|
||||||
|
Scenario: POST /foo/forms/pdfengines/rotate (Root Path)
|
||||||
|
Given I have a Gotenberg container with the following environment variable(s):
|
||||||
|
| API_ENABLE_DEBUG_ROUTE | true |
|
||||||
|
| API_ROOT_PATH | /foo/ |
|
||||||
|
When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/page_1.pdf | file |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/pdf"
|
||||||
@@ -499,6 +499,18 @@ Feature: /forms/pdfengines/split
|
|||||||
Then the response header "Content-Type" should be "application/zip"
|
Then the response header "Content-Type" should be "application/zip"
|
||||||
Then there should be 2 PDF(s) in the response
|
Then there should be 2 PDF(s) in the response
|
||||||
|
|
||||||
|
@rotate
|
||||||
|
Scenario: POST /forms/pdfengines/split (Rotate 90)
|
||||||
|
Given I have a default Gotenberg container
|
||||||
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
|
||||||
|
| files | testdata/pages_3.pdf | file |
|
||||||
|
| splitMode | intervals | field |
|
||||||
|
| splitSpan | 2 | field |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
|
Then the response status code should be 200
|
||||||
|
Then the response header "Content-Type" should be "application/zip"
|
||||||
|
Then there should be 2 PDF(s) in the response
|
||||||
|
|
||||||
@embed
|
@embed
|
||||||
Scenario: POST /foo/forms/pdfengines/split (Embeds)
|
Scenario: POST /foo/forms/pdfengines/split (Embeds)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
@@ -522,9 +534,10 @@ Feature: /forms/pdfengines/split
|
|||||||
@metadata
|
@metadata
|
||||||
@watermark
|
@watermark
|
||||||
@stamp
|
@stamp
|
||||||
|
@rotate
|
||||||
@flatten
|
@flatten
|
||||||
@embed
|
@embed
|
||||||
Scenario: POST /forms/pdfengines/split (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Flatten & Embeds)
|
Scenario: POST /forms/pdfengines/split (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Rotate & Flatten & Embeds)
|
||||||
Given I have a default Gotenberg container
|
Given I have a default Gotenberg container
|
||||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
|
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
|
||||||
| files | testdata/pages_3.pdf | file |
|
| files | testdata/pages_3.pdf | file |
|
||||||
@@ -537,6 +550,7 @@ Feature: /forms/pdfengines/split
|
|||||||
| watermarkExpression | CONFIDENTIAL | field |
|
| watermarkExpression | CONFIDENTIAL | field |
|
||||||
| stampSource | text | field |
|
| stampSource | text | field |
|
||||||
| stampExpression | DRAFT | field |
|
| stampExpression | DRAFT | field |
|
||||||
|
| rotateAngle | 90 | field |
|
||||||
| flatten | true | field |
|
| flatten | true | field |
|
||||||
| embeds | testdata/embed_1.xml | file |
|
| embeds | testdata/embed_1.xml | file |
|
||||||
| embeds | testdata/embed_2.xml | file |
|
| embeds | testdata/embed_2.xml | file |
|
||||||
|
|||||||
Reference in New Issue
Block a user