mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-14 11:22:15 +01:00
feat(pdfengines): add watermark and stamp feature
This commit is contained in:
@@ -22,6 +22,8 @@ type multiPdfEngines struct {
|
||||
embedEngines []gotenberg.PdfEngine
|
||||
readBookmarksEngines []gotenberg.PdfEngine
|
||||
writeBookmarksEngines []gotenberg.PdfEngine
|
||||
watermarkEngines []gotenberg.PdfEngine
|
||||
stampEngines []gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
func newMultiPdfEngines(
|
||||
@@ -34,7 +36,9 @@ func newMultiPdfEngines(
|
||||
passwordEngines,
|
||||
embedEngines,
|
||||
readBookmarksEngines,
|
||||
writeBookmarksEngines []gotenberg.PdfEngine,
|
||||
writeBookmarksEngines,
|
||||
watermarkEngines,
|
||||
stampEngines []gotenberg.PdfEngine,
|
||||
) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
mergeEngines: mergeEngines,
|
||||
@@ -47,6 +51,8 @@ func newMultiPdfEngines(
|
||||
embedEngines: embedEngines,
|
||||
readBookmarksEngines: readBookmarksEngines,
|
||||
writeBookmarksEngines: writeBookmarksEngines,
|
||||
watermarkEngines: watermarkEngines,
|
||||
stampEngines: stampEngines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +375,56 @@ func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *zap.Logger
|
||||
return fmt.Errorf("embed files into PDF using multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Watermark applies a watermark (behind page content) to a PDF file using the
|
||||
// first available engine that supports watermarking.
|
||||
func (multi *multiPdfEngines) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.watermarkEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Watermark(ctx, logger, inputPath, stamp)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case watermarkErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, watermarkErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("watermark PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Stamp applies a stamp (on top of page content) to a PDF file using the
|
||||
// first available engine that supports stamping.
|
||||
func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.stampEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Stamp(ctx, logger, inputPath, stamp)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case stampErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, stampErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||
|
||||
@@ -38,6 +38,8 @@ type PdfEngines struct {
|
||||
embedNames []string
|
||||
readBookmarksNames []string
|
||||
writeBookmarksNames []string
|
||||
watermarkNames []string
|
||||
stampNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
@@ -58,6 +60,8 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.StringSlice("pdfengines-embed-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the file embedding feature - empty means all")
|
||||
fs.StringSlice("pdfengines-read-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the read 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-stamp-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the stamp feature - empty means all")
|
||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||
|
||||
// Deprecated flags.
|
||||
@@ -87,6 +91,8 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
embedNames := flags.MustStringSlice("pdfengines-embed-engines")
|
||||
readBookmarksNames := flags.MustStringSlice("pdfengines-read-bookmarks-engines")
|
||||
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-bookmarks-engines")
|
||||
watermarkNames := flags.MustStringSlice("pdfengines-watermark-engines")
|
||||
stampNames := flags.MustStringSlice("pdfengines-stamp-engines")
|
||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||
|
||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||
@@ -163,6 +169,16 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.writeBookmarksNames = writeBookmarksNames
|
||||
}
|
||||
|
||||
mod.watermarkNames = defaultNames
|
||||
if len(watermarkNames) > 0 {
|
||||
mod.watermarkNames = watermarkNames
|
||||
}
|
||||
|
||||
mod.stampNames = defaultNames
|
||||
if len(stampNames) > 0 {
|
||||
mod.stampNames = stampNames
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -214,6 +230,8 @@ func (mod *PdfEngines) Validate() error {
|
||||
findNonExistingEngines(mod.embedNames)
|
||||
findNonExistingEngines(mod.readBookmarksNames)
|
||||
findNonExistingEngines(mod.writeBookmarksNames)
|
||||
findNonExistingEngines(mod.watermarkNames)
|
||||
findNonExistingEngines(mod.stampNames)
|
||||
|
||||
if len(nonExistingEngines) == 0 {
|
||||
return nil
|
||||
@@ -236,6 +254,8 @@ func (mod *PdfEngines) SystemMessages() []string {
|
||||
fmt.Sprintf("embed engines - %s", strings.Join(mod.embedNames[:], " ")),
|
||||
fmt.Sprintf("read bookmarks engines - %s", strings.Join(mod.readBookmarksNames[:], " ")),
|
||||
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames[:], " ")),
|
||||
fmt.Sprintf("watermark engines - %s", strings.Join(mod.watermarkNames[:], " ")),
|
||||
fmt.Sprintf("stamp engines - %s", strings.Join(mod.stampNames[:], " ")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +286,8 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
engines(mod.embedNames),
|
||||
engines(mod.readBookmarksNames),
|
||||
engines(mod.writeBookmarksNames),
|
||||
engines(mod.watermarkNames),
|
||||
engines(mod.stampNames),
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -293,6 +315,8 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
writeBookmarksRoute(engine),
|
||||
encryptRoute(engine),
|
||||
embedRoute(engine),
|
||||
watermarkRoute(engine),
|
||||
stampRoute(engine),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -391,6 +391,122 @@ func EmbedFilesStub(ctx *api.Context, engine gotenberg.PdfEngine, embedPaths []s
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormDataPdfWatermark creates a [gotenberg.Stamp] for watermarking from the
|
||||
// form data.
|
||||
func FormDataPdfWatermark(form *api.FormData, mandatory bool) gotenberg.Stamp {
|
||||
return formDataPdfStampOrWatermark(form, "watermark", mandatory)
|
||||
}
|
||||
|
||||
// FormDataPdfStamp creates a [gotenberg.Stamp] for stamping from the form data.
|
||||
func FormDataPdfStamp(form *api.FormData, mandatory bool) gotenberg.Stamp {
|
||||
return formDataPdfStampOrWatermark(form, "stamp", mandatory)
|
||||
}
|
||||
|
||||
func formDataPdfStampOrWatermark(form *api.FormData, prefix string, mandatory bool) gotenberg.Stamp {
|
||||
var (
|
||||
source string
|
||||
expression string
|
||||
pages string
|
||||
options map[string]string
|
||||
)
|
||||
|
||||
sourceFunc := func(value string) error {
|
||||
if value != "" && value != gotenberg.StampSourceText && value != gotenberg.StampSourceImage && value != gotenberg.StampSourcePDF {
|
||||
return fmt.Errorf("wrong value, expected either '%s', '%s' or '%s'", gotenberg.StampSourceText, gotenberg.StampSourceImage, gotenberg.StampSourcePDF)
|
||||
}
|
||||
source = value
|
||||
return nil
|
||||
}
|
||||
|
||||
optionsFunc := func(value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(value), &options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal %s options: %w", prefix, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.
|
||||
MandatoryCustom(prefix+"Source", func(value string) error {
|
||||
return sourceFunc(value)
|
||||
}).
|
||||
String(prefix+"Expression", &expression, "").
|
||||
String(prefix+"Pages", &pages, "").
|
||||
Custom(prefix+"Options", func(value string) error {
|
||||
return optionsFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.
|
||||
Custom(prefix+"Source", func(value string) error {
|
||||
return sourceFunc(value)
|
||||
}).
|
||||
String(prefix+"Expression", &expression, "").
|
||||
String(prefix+"Pages", &pages, "").
|
||||
Custom(prefix+"Options", func(value string) error {
|
||||
return optionsFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
return gotenberg.Stamp{
|
||||
Source: source,
|
||||
Expression: expression,
|
||||
Pages: pages,
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// FormDataPdfWatermarkFiles extracts watermark file paths from form data.
|
||||
func FormDataPdfWatermarkFiles(form *api.FormData) []string {
|
||||
var paths []string
|
||||
form.Watermarks(&paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
// FormDataPdfStampFiles extracts stamp file paths from form data.
|
||||
func FormDataPdfStampFiles(form *api.FormData) []string {
|
||||
var paths []string
|
||||
form.Stamps(&paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
// WatermarkStub applies a watermark to a list of PDF files. If the stamp has
|
||||
// no source, it does nothing.
|
||||
func WatermarkStub(ctx *api.Context, engine gotenberg.PdfEngine, stamp gotenberg.Stamp, inputPaths []string) error {
|
||||
if stamp.Source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Watermark(ctx, ctx.Log(), inputPath, stamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("watermark '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StampStub applies a stamp to a list of PDF files. If the stamp has
|
||||
// no source, it does nothing.
|
||||
func StampStub(ctx *api.Context, engine gotenberg.PdfEngine, stamp gotenberg.Stamp, inputPaths []string) error {
|
||||
if stamp.Source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Stamp(ctx, ctx.Log(), inputPath, stamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stamp '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
@@ -406,6 +522,10 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
bookmarks := FormDataPdfBookmarks(form, false)
|
||||
userPassword, ownerPassword := FormDataPdfEncrypt(form)
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
watermark := FormDataPdfWatermark(form, false)
|
||||
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
||||
stamp := FormDataPdfStamp(form, false)
|
||||
stampFiles := FormDataPdfStampFiles(form)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
@@ -419,6 +539,13 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && len(watermarkFiles) > 0 {
|
||||
watermark.Expression = watermarkFiles[0]
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && len(stampFiles) > 0 {
|
||||
stamp.Expression = stampFiles[0]
|
||||
}
|
||||
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
||||
if err != nil {
|
||||
@@ -430,6 +557,16 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
}
|
||||
|
||||
err = WatermarkStub(ctx, engine, watermark, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("watermark PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = StampStub(ctx, engine, stamp, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stamp PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
@@ -520,6 +657,10 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := FormDataPdfEncrypt(form)
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
watermark := FormDataPdfWatermark(form, false)
|
||||
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
||||
stamp := FormDataPdfStamp(form, false)
|
||||
stampFiles := FormDataPdfStampFiles(form)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
@@ -531,6 +672,13 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && len(watermarkFiles) > 0 {
|
||||
watermark.Expression = watermarkFiles[0]
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && len(stampFiles) > 0 {
|
||||
stamp.Expression = stampFiles[0]
|
||||
}
|
||||
|
||||
outputPaths, err := SplitPdfStub(ctx, engine, mode, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("split PDFs: %w", err)
|
||||
@@ -541,6 +689,16 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = WatermarkStub(ctx, engine, watermark, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("watermark PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = StampStub(ctx, engine, stamp, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stamp PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
@@ -904,3 +1062,105 @@ func embedRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// watermarkRoute returns an [api.Route] which can add watermarks to PDFs.
|
||||
//
|
||||
//nolint:dupl
|
||||
func watermarkRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/watermark",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
stamp := FormDataPdfWatermark(form, true)
|
||||
watermarkFiles := FormDataPdfWatermarkFiles(form)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF {
|
||||
if len(watermarkFiles) == 0 {
|
||||
return api.WrapError(
|
||||
errors.New("no watermark file provided"),
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusBadRequest,
|
||||
"Invalid form data: a watermark file is required for image or pdf source",
|
||||
),
|
||||
)
|
||||
}
|
||||
stamp.Expression = watermarkFiles[0]
|
||||
}
|
||||
|
||||
err = WatermarkStub(ctx, engine, stamp, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("watermark PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// stampRoute returns an [api.Route] which can add stamps to PDFs.
|
||||
//
|
||||
//nolint:dupl
|
||||
func stampRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/stamp",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
stamp := FormDataPdfStamp(form, true)
|
||||
stampFiles := FormDataPdfStampFiles(form)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF {
|
||||
if len(stampFiles) == 0 {
|
||||
return api.WrapError(
|
||||
errors.New("no stamp file provided"),
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusBadRequest,
|
||||
"Invalid form data: a stamp file is required for image or pdf source",
|
||||
),
|
||||
)
|
||||
}
|
||||
stamp.Expression = stampFiles[0]
|
||||
}
|
||||
|
||||
err = StampStub(ctx, engine, stamp, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stamp PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user