feat(pdfengines): add watermark and stamp feature

This commit is contained in:
Julien Neuhart
2026-03-18 04:46:12 +01:00
parent 4ac493250c
commit 19db80bc2e
26 changed files with 1351 additions and 50 deletions

View File

@@ -26,7 +26,7 @@ make test-integration TAGS=chromium-convert-html
make test-integration TAGS="merge,split" make test-integration TAGS="merge,split"
``` ```
Available tags: `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-convert-markdown`, `chromium-convert-url`, `debug`, `health`, `libreoffice`, `libreoffice-convert`, `output-filename`, `pdfengines`, `pdfengines-convert`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-flatten`, `flatten`, `pdfengines-merge`, `merge`, `pdfengines-metadata`, `metadata`, `pdfengines-split`, `split`, `pdfengines-bookmarks`, `bookmarks`, `prometheus-metrics`, `root`, `version`, `webhook`, `download-from`. Available tags: `chromium`, `chromium-concurrent`, `chromium-convert-html`, `chromium-convert-markdown`, `chromium-convert-url`, `debug`, `health`, `libreoffice`, `libreoffice-convert`, `output-filename`, `pdfengines`, `pdfengines-convert`, `pdfengines-embed`, `embed`, `pdfengines-encrypt`, `encrypt`, `pdfengines-flatten`, `flatten`, `pdfengines-merge`, `merge`, `pdfengines-metadata`, `metadata`, `pdfengines-split`, `split`, `pdfengines-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-bookmarks`, `bookmarks`, `prometheus-metrics`, `root`, `version`, `webhook`, `download-from`.
Other useful flags: Other useful flags:

View File

@@ -195,6 +195,10 @@ NO_CONCURRENCY=false
# metadata # metadata
# pdfengines-split # pdfengines-split
# split # split
# pdfengines-watermark
# watermark
# pdfengines-stamp
# stamp
# pdfengines-bookmarks # pdfengines-bookmarks
# bookmarks # bookmarks
# prometheus-metrics # prometheus-metrics

24
go.sum
View File

@@ -166,8 +166,6 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=
github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=
github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/minio/minlz v1.1.0 h1:rUOGu3EP4EqJC5k3qCsIwEnZiJULKqtRyDdqbhlvMmQ= github.com/minio/minlz v1.1.0 h1:rUOGu3EP4EqJC5k3qCsIwEnZiJULKqtRyDdqbhlvMmQ=
github.com/minio/minlz v1.1.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/minio/minlz v1.1.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -198,8 +196,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -214,8 +210,6 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -265,30 +259,20 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
@@ -299,8 +283,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
@@ -322,8 +304,6 @@ golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -57,6 +57,8 @@ type PdfEngineMock struct {
EncryptMock func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error EncryptMock func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error
EmbedFilesMock func(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error EmbedFilesMock func(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error
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
StampMock func(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) 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 {
@@ -103,6 +105,14 @@ func (engine *PdfEngineMock) WriteBookmarks(ctx context.Context, logger *zap.Log
return engine.WriteBookmarksMock(ctx, logger, inputPath, bookmarks) return engine.WriteBookmarksMock(ctx, logger, inputPath, bookmarks)
} }
func (engine *PdfEngineMock) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error {
return engine.WatermarkMock(ctx, logger, inputPath, stamp)
}
func (engine *PdfEngineMock) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error {
return engine.StampMock(ctx, logger, inputPath, stamp)
}
// 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)

View File

@@ -28,6 +28,10 @@ var (
// ErrPdfEncryptionNotSupported is returned when encryption // ErrPdfEncryptionNotSupported is returned when encryption
// is not supported by the PDF engine. // is not supported by the PDF engine.
ErrPdfEncryptionNotSupported = errors.New("encryption not supported") ErrPdfEncryptionNotSupported = errors.New("encryption not supported")
// ErrPdfStampSourceNotSupported is returned when a stamp source type
// is not supported by the PDF engine.
ErrPdfStampSourceNotSupported = errors.New("stamp source not supported")
) )
// PdfEngineInvalidArgsError represents an error returned by a PDF engine when // PdfEngineInvalidArgsError represents an error returned by a PDF engine when
@@ -49,6 +53,33 @@ func NewPdfEngineInvalidArgs(engine, msg string) error {
return &PdfEngineInvalidArgsError{engine, msg} return &PdfEngineInvalidArgsError{engine, msg}
} }
const (
// StampSourceText represents a text-based stamp source.
StampSourceText string = "text"
// StampSourceImage represents an image-based stamp source.
StampSourceImage string = "image"
// StampSourcePDF represents a PDF-based stamp source.
StampSourcePDF string = "pdf"
)
// Stamp gathers the data required to apply a watermark or stamp to a PDF.
type Stamp struct {
// Source is one of "text", "image", or "pdf".
Source string
// Expression is the text content (for text source) or file path (for
// image/pdf source).
Expression string
// Pages is the optional page range to apply the stamp to.
Pages string
// Options holds engine-specific styling options.
Options map[string]string
}
const ( const (
// SplitModeIntervals represents a mode where a PDF is split at specific // SplitModeIntervals represents a mode where a PDF is split at specific
// intervals. // intervals.
@@ -166,6 +197,12 @@ type PdfEngine interface {
// without modifying the main PDF content. // without modifying the main PDF content.
// TODO: attachments instead? Rename the route? // TODO: attachments instead? Rename the route?
EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error
// Watermark applies a watermark (behind page content) to a PDF file.
Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp Stamp) error
// 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
} }
// PdfEngineProvider offers an interface to instantiate a [PdfEngine]. // PdfEngineProvider offers an interface to instantiate a [PdfEngine].

View File

@@ -17,9 +17,15 @@ import (
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
) )
// EmbedsFormField represents the form field name for embedding files.
const ( const (
// EmbedsFormField represents the form field name for embedding files.
EmbedsFormField string = "embeds" EmbedsFormField string = "embeds"
// WatermarksFormField represents the form field name for watermark files.
WatermarksFormField string = "watermarks"
// StampsFormField represents the form field name for stamp files.
StampsFormField string = "stamps"
) )
// FormData is a helper for validating and hydrating values from a // FormData is a helper for validating and hydrating values from a
@@ -406,17 +412,57 @@ func (form *FormData) MandatoryPaths(extensions []string, target *[]string) *For
return form return form
} }
// Watermarks binds the absolute paths of form data files that should be
// used as watermark sources. Only files uploaded with the "watermarks"
// field name will be included.
func (form *FormData) Watermarks(target *[]string) *FormData {
if form.errors != nil {
return form
}
if paths, ok := form.filesByField[WatermarksFormField]; ok {
*target = append(*target, paths...)
}
return form
}
// Stamps binds the absolute paths of form data files that should be
// used as stamp sources. Only files uploaded with the "stamps"
// field name will be included.
func (form *FormData) Stamps(target *[]string) *FormData {
if form.errors != nil {
return form
}
if paths, ok := form.filesByField[StampsFormField]; ok {
*target = append(*target, paths...)
}
return form
}
// paths bind the absolute paths of form data files, according to a list of // paths bind the absolute paths of form data files, according to a list of
// file extensions, to a string slice variable. // file extensions, to a string slice variable.
// embeds are excluded. // embeds, watermarks, and stamps are excluded.
func (form *FormData) paths(extensions []string, target *[]string) *FormData { func (form *FormData) paths(extensions []string, target *[]string) *FormData {
embeds, ok := form.filesByField[EmbedsFormField] embeds, ok := form.filesByField[EmbedsFormField]
watermarks, wmOk := form.filesByField[WatermarksFormField]
stamps, stOk := form.filesByField[StampsFormField]
for filename, path := range form.files { for filename, path := range form.files {
if ok && slices.Contains(embeds, path) { if ok && slices.Contains(embeds, path) {
continue continue
} }
if wmOk && slices.Contains(watermarks, path) {
continue
}
if stOk && slices.Contains(stamps, path) {
continue
}
for _, ext := range extensions { for _, ext := range extensions {
// See https://github.com/gotenberg/gotenberg/issues/228. // See https://github.com/gotenberg/gotenberg/issues/228.
if strings.ToLower(filepath.Ext(filename)) == ext { if strings.ToLower(filepath.Ext(filename)) == ext {

View File

@@ -61,6 +61,10 @@ func ParseError(err error) (int, string) {
return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues" return http.StatusBadRequest, "At least one PDF engine cannot process the requested metadata, while others may have failed to convert due to different issues"
} }
if errors.Is(err, gotenberg.ErrPdfStampSourceNotSupported) {
return http.StatusBadRequest, "At least one PDF engine cannot process the requested stamp source type, 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()

View File

@@ -415,6 +415,10 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form) userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form) embedPaths := pdfengines.FormDataPdfEmbeds(form)
watermark := pdfengines.FormDataPdfWatermark(form, false)
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
stamp := pdfengines.FormDataPdfStamp(form, false)
stampFiles := pdfengines.FormDataPdfStampFiles(form)
var url string var url string
err := form. err := form.
@@ -424,7 +428,14 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) return fmt.Errorf("validate form data: %w", err)
} }
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths) 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]
}
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp)
if err != nil { if err != nil {
return fmt.Errorf("convert URL to PDF: %w", err) return fmt.Errorf("convert URL to PDF: %w", err)
} }
@@ -478,6 +489,10 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form) userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form) embedPaths := pdfengines.FormDataPdfEmbeds(form)
watermark := pdfengines.FormDataPdfWatermark(form, false)
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
stamp := pdfengines.FormDataPdfStamp(form, false)
stampFiles := pdfengines.FormDataPdfStampFiles(form)
var inputPath string var inputPath string
err := form. err := form.
@@ -487,8 +502,15 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) 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]
}
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) err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp)
if err != nil { if err != nil {
return fmt.Errorf("convert HTML to PDF: %w", err) return fmt.Errorf("convert HTML to PDF: %w", err)
} }
@@ -543,6 +565,10 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form) userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form) embedPaths := pdfengines.FormDataPdfEmbeds(form)
watermark := pdfengines.FormDataPdfWatermark(form, false)
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
stamp := pdfengines.FormDataPdfStamp(form, false)
stampFiles := pdfengines.FormDataPdfStampFiles(form)
var ( var (
inputPath string inputPath string
@@ -557,12 +583,19 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) 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]
}
url, err := markdownToHtml(ctx, inputPath, markdownPaths) url, err := markdownToHtml(ctx, inputPath, markdownPaths)
if err != nil { if err != nil {
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) err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp)
if err != nil { if err != nil {
return fmt.Errorf("convert markdown to PDF: %w", err) return fmt.Errorf("convert markdown to PDF: %w", err)
} }
@@ -686,7 +719,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) 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) 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)
@@ -758,6 +791,16 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
return fmt.Errorf("convert PDF(s): %w", err) return fmt.Errorf("convert PDF(s): %w", err)
} }
err = pdfengines.WatermarkStub(ctx, engine, watermark, convertOutputPaths)
if err != nil {
return fmt.Errorf("watermark PDFs: %w", err)
}
err = pdfengines.StampStub(ctx, engine, stamp, convertOutputPaths)
if err != nil {
return fmt.Errorf("stamp PDFs: %w", err)
}
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths) err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
if err != nil { if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err) return fmt.Errorf("embed files into PDFs: %w", err)

View File

@@ -254,6 +254,16 @@ func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *zap.Logger, file
return fmt.Errorf("embed files with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("embed files with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Watermark is not available in this implementation.
func (engine *ExifTool) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("watermark PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Stamp is not available in this implementation.
func (engine *ExifTool) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("stamp PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*ExifTool)(nil) _ gotenberg.Module = (*ExifTool)(nil)

View File

@@ -147,6 +147,29 @@ type Options struct {
// Possible values are: 75, 150, 300, 600 and 1200. // Possible values are: 75, 150, 300, 600 and 1200.
MaxImageResolution int MaxImageResolution int
// NativeWatermarkText specifies the text for a watermark to be drawn on
// every page of the exported PDF file.
// See https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html.
NativeWatermarkText string
// NativeWatermarkColor specifies the color for the watermark text as a
// decimal long value. Default is 8388223 (light green).
NativeWatermarkColor int
// NativeWatermarkFontHeight specifies the font size for the watermark text.
NativeWatermarkFontHeight int
// NativeWatermarkRotateAngle specifies the rotation angle for the watermark
// text in tenths of a degree (e.g., 450 = 45°).
NativeWatermarkRotateAngle int
// NativeWatermarkFontName specifies the font name for the watermark text.
// Default is "Helvetica".
NativeWatermarkFontName string
// NativeTiledWatermarkText specifies the tiled watermark text.
NativeTiledWatermarkText string
// PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b, // PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b,
// PDF/A-3b and PDF/UA. // PDF/A-3b and PDF/UA.
PdfFormats gotenberg.PdfFormats PdfFormats gotenberg.PdfFormats
@@ -178,6 +201,12 @@ func DefaultOptions() Options {
Quality: 90, Quality: 90,
ReduceImageResolution: false, ReduceImageResolution: false,
MaxImageResolution: 300, MaxImageResolution: 300,
NativeWatermarkText: "",
NativeWatermarkColor: 8388223,
NativeWatermarkFontHeight: 0,
NativeWatermarkRotateAngle: 0,
NativeWatermarkFontName: "Helvetica",
NativeTiledWatermarkText: "",
PdfFormats: gotenberg.PdfFormats{ PdfFormats: gotenberg.PdfFormats{
PdfA: "", PdfA: "",
PdfUa: false, PdfUa: false,

View File

@@ -302,6 +302,30 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
args = append(args, "--export", fmt.Sprintf("ReduceImageResolution=%t", options.ReduceImageResolution)) args = append(args, "--export", fmt.Sprintf("ReduceImageResolution=%t", options.ReduceImageResolution))
args = append(args, "--export", fmt.Sprintf("MaxImageResolution=%d", options.MaxImageResolution)) args = append(args, "--export", fmt.Sprintf("MaxImageResolution=%d", options.MaxImageResolution))
if options.NativeWatermarkText != "" {
args = append(args, "--export", fmt.Sprintf("Watermark=%s", options.NativeWatermarkText))
}
if options.NativeWatermarkColor != 0 {
args = append(args, "--export", fmt.Sprintf("WatermarkColor=%d", options.NativeWatermarkColor))
}
if options.NativeWatermarkFontHeight > 0 {
args = append(args, "--export", fmt.Sprintf("WatermarkFontHeight=%d", options.NativeWatermarkFontHeight))
}
if options.NativeWatermarkRotateAngle != 0 {
args = append(args, "--export", fmt.Sprintf("WatermarkRotateAngle=%d", options.NativeWatermarkRotateAngle))
}
if options.NativeWatermarkFontName != "" && options.NativeWatermarkFontName != "Helvetica" {
args = append(args, "--export", fmt.Sprintf("WatermarkFontName=%s", options.NativeWatermarkFontName))
}
if options.NativeTiledWatermarkText != "" {
args = append(args, "--export", fmt.Sprintf("TiledWatermark=%s", options.NativeTiledWatermarkText))
}
switch options.PdfFormats.PdfA { switch options.PdfFormats.PdfA {
case "": case "":
case gotenberg.PdfA1b: case gotenberg.PdfA1b:

View File

@@ -116,6 +116,16 @@ func (engine *LibreOfficePdfEngine) EmbedFiles(ctx context.Context, logger *zap.
return fmt.Errorf("embed files with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("embed files with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Watermark is not available in this implementation.
func (engine *LibreOfficePdfEngine) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("watermark PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Stamp is not available in this implementation.
func (engine *LibreOfficePdfEngine) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("stamp PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil) _ gotenberg.Module = (*LibreOfficePdfEngine)(nil)

View File

@@ -32,6 +32,10 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
metadata := pdfengines.FormDataPdfMetadata(form, false) metadata := pdfengines.FormDataPdfMetadata(form, false)
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form) userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
embedPaths := pdfengines.FormDataPdfEmbeds(form) embedPaths := pdfengines.FormDataPdfEmbeds(form)
watermark := pdfengines.FormDataPdfWatermark(form, false)
watermarkFiles := pdfengines.FormDataPdfWatermarkFiles(form)
stamp := pdfengines.FormDataPdfStamp(form, false)
stampFiles := pdfengines.FormDataPdfStampFiles(form)
zeroValuedSplitMode := gotenberg.SplitMode{} zeroValuedSplitMode := gotenberg.SplitMode{}
@@ -60,6 +64,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
quality int quality int
reduceImageResolution bool reduceImageResolution bool
maxImageResolution int maxImageResolution int
nativeWatermarkText string
nativeWatermarkColor int
nativeWatermarkFontHeight int
nativeWatermarkRotateAngle int
nativeWatermarkFontName string
nativeTiledWatermarkText string
nativePdfFormats bool nativePdfFormats bool
merge bool merge bool
flatten bool flatten bool
@@ -128,6 +138,48 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
maxImageResolution = intValue maxImageResolution = intValue
return nil return nil
}). }).
String("nativeWatermarkText", &nativeWatermarkText, defaultOptions.NativeWatermarkText).
Custom("nativeWatermarkColor", func(value string) error {
if value == "" {
nativeWatermarkColor = defaultOptions.NativeWatermarkColor
return nil
}
intValue, err := strconv.Atoi(value)
if err != nil {
return err
}
nativeWatermarkColor = intValue
return nil
}).
Custom("nativeWatermarkFontHeight", func(value string) error {
if value == "" {
nativeWatermarkFontHeight = defaultOptions.NativeWatermarkFontHeight
return nil
}
intValue, err := strconv.Atoi(value)
if err != nil {
return err
}
if intValue < 0 {
return errors.New("value is inferior to 0")
}
nativeWatermarkFontHeight = intValue
return nil
}).
Custom("nativeWatermarkRotateAngle", func(value string) error {
if value == "" {
nativeWatermarkRotateAngle = defaultOptions.NativeWatermarkRotateAngle
return nil
}
intValue, err := strconv.Atoi(value)
if err != nil {
return err
}
nativeWatermarkRotateAngle = intValue
return nil
}).
String("nativeWatermarkFontName", &nativeWatermarkFontName, defaultOptions.NativeWatermarkFontName).
String("nativeTiledWatermarkText", &nativeTiledWatermarkText, defaultOptions.NativeTiledWatermarkText).
Bool("nativePdfFormats", &nativePdfFormats, true). Bool("nativePdfFormats", &nativePdfFormats, true).
Bool("merge", &merge, false). Bool("merge", &merge, false).
Bool("flatten", &flatten, false). Bool("flatten", &flatten, false).
@@ -136,6 +188,13 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
return fmt.Errorf("validate form data: %w", err) 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 := make([]string, len(inputPaths)) outputPaths := make([]string, len(inputPaths))
for i, inputPath := range inputPaths { for i, inputPath := range inputPaths {
outputPaths[i] = ctx.GeneratePath(".pdf") outputPaths[i] = ctx.GeneratePath(".pdf")
@@ -163,6 +222,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
Quality: quality, Quality: quality,
ReduceImageResolution: reduceImageResolution, ReduceImageResolution: reduceImageResolution,
MaxImageResolution: maxImageResolution, MaxImageResolution: maxImageResolution,
NativeWatermarkText: nativeWatermarkText,
NativeWatermarkColor: nativeWatermarkColor,
NativeWatermarkFontHeight: nativeWatermarkFontHeight,
NativeWatermarkRotateAngle: nativeWatermarkRotateAngle,
NativeWatermarkFontName: nativeWatermarkFontName,
NativeTiledWatermarkText: nativeTiledWatermarkText,
} }
if nativePdfFormats && splitMode == zeroValuedSplitMode { if nativePdfFormats && splitMode == zeroValuedSplitMode {
@@ -253,6 +318,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
} }
} }
err = pdfengines.WatermarkStub(ctx, engine, watermark, outputPaths)
if err != nil {
return fmt.Errorf("watermark PDFs: %w", err)
}
err = pdfengines.StampStub(ctx, engine, stamp, outputPaths)
if err != nil {
return fmt.Errorf("stamp PDFs: %w", err)
}
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, outputPaths) err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
if err != nil { if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err) return fmt.Errorf("embed files into PDFs: %w", err)

View File

@@ -379,6 +379,57 @@ func (engine *PdfCpu) Encrypt(ctx context.Context, logger *zap.Logger, inputPath
return nil return nil
} }
// Watermark applies a watermark (behind page content) to a PDF file using pdfcpu.
func (engine *PdfCpu) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return engine.applyStampOrWatermark(ctx, logger, "watermark", inputPath, stamp)
}
// Stamp applies a stamp (on top of page content) to a PDF file using pdfcpu.
func (engine *PdfCpu) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return engine.applyStampOrWatermark(ctx, logger, "stamp", inputPath, stamp)
}
func (engine *PdfCpu) applyStampOrWatermark(ctx context.Context, logger *zap.Logger, command string, inputPath string, stamp gotenberg.Stamp) error {
var mode string
switch stamp.Source {
case gotenberg.StampSourceText:
mode = "text"
case gotenberg.StampSourceImage:
mode = "image"
case gotenberg.StampSourcePDF:
mode = "pdf"
default:
return fmt.Errorf("%s PDF with pdfcpu: %w", command, gotenberg.ErrPdfStampSourceNotSupported)
}
// Build description from Options map.
var descParts []string
for k, v := range stamp.Options {
descParts = append(descParts, fmt.Sprintf("%s:%s", k, v))
}
description := strings.Join(descParts, ", ")
args := []string{command, "add", "-mode", mode}
if stamp.Pages != "" {
args = append(args, "-pages", stamp.Pages)
}
args = append(args, "--", stamp.Expression, description, inputPath, 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("%s PDF with pdfcpu: %w", command, err)
}
return nil
}
// Interface guards. // Interface guards.
var ( var (
_ gotenberg.Module = (*PdfCpu)(nil) _ gotenberg.Module = (*PdfCpu)(nil)

View File

@@ -22,6 +22,8 @@ type multiPdfEngines struct {
embedEngines []gotenberg.PdfEngine embedEngines []gotenberg.PdfEngine
readBookmarksEngines []gotenberg.PdfEngine readBookmarksEngines []gotenberg.PdfEngine
writeBookmarksEngines []gotenberg.PdfEngine writeBookmarksEngines []gotenberg.PdfEngine
watermarkEngines []gotenberg.PdfEngine
stampEngines []gotenberg.PdfEngine
} }
func newMultiPdfEngines( func newMultiPdfEngines(
@@ -34,7 +36,9 @@ func newMultiPdfEngines(
passwordEngines, passwordEngines,
embedEngines, embedEngines,
readBookmarksEngines, readBookmarksEngines,
writeBookmarksEngines []gotenberg.PdfEngine, writeBookmarksEngines,
watermarkEngines,
stampEngines []gotenberg.PdfEngine,
) *multiPdfEngines { ) *multiPdfEngines {
return &multiPdfEngines{ return &multiPdfEngines{
mergeEngines: mergeEngines, mergeEngines: mergeEngines,
@@ -47,6 +51,8 @@ func newMultiPdfEngines(
embedEngines: embedEngines, embedEngines: embedEngines,
readBookmarksEngines: readBookmarksEngines, readBookmarksEngines: readBookmarksEngines,
writeBookmarksEngines: writeBookmarksEngines, 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) 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. // Interface guards.
var ( var (
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil) _ gotenberg.PdfEngine = (*multiPdfEngines)(nil)

View File

@@ -38,6 +38,8 @@ type PdfEngines struct {
embedNames []string embedNames []string
readBookmarksNames []string readBookmarksNames []string
writeBookmarksNames []string writeBookmarksNames []string
watermarkNames []string
stampNames []string
engines []gotenberg.PdfEngine engines []gotenberg.PdfEngine
disableRoutes bool 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-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-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-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") fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
// Deprecated flags. // Deprecated flags.
@@ -87,6 +91,8 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
embedNames := flags.MustStringSlice("pdfengines-embed-engines") embedNames := flags.MustStringSlice("pdfengines-embed-engines")
readBookmarksNames := flags.MustStringSlice("pdfengines-read-bookmarks-engines") readBookmarksNames := flags.MustStringSlice("pdfengines-read-bookmarks-engines")
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-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") mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
engines, err := ctx.Modules(new(gotenberg.PdfEngine)) engines, err := ctx.Modules(new(gotenberg.PdfEngine))
@@ -163,6 +169,16 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
mod.writeBookmarksNames = writeBookmarksNames 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 return nil
} }
@@ -214,6 +230,8 @@ func (mod *PdfEngines) Validate() error {
findNonExistingEngines(mod.embedNames) findNonExistingEngines(mod.embedNames)
findNonExistingEngines(mod.readBookmarksNames) findNonExistingEngines(mod.readBookmarksNames)
findNonExistingEngines(mod.writeBookmarksNames) findNonExistingEngines(mod.writeBookmarksNames)
findNonExistingEngines(mod.watermarkNames)
findNonExistingEngines(mod.stampNames)
if len(nonExistingEngines) == 0 { if len(nonExistingEngines) == 0 {
return nil return nil
@@ -236,6 +254,8 @@ func (mod *PdfEngines) SystemMessages() []string {
fmt.Sprintf("embed engines - %s", strings.Join(mod.embedNames[:], " ")), fmt.Sprintf("embed engines - %s", strings.Join(mod.embedNames[:], " ")),
fmt.Sprintf("read bookmarks engines - %s", strings.Join(mod.readBookmarksNames[:], " ")), fmt.Sprintf("read bookmarks engines - %s", strings.Join(mod.readBookmarksNames[:], " ")),
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("stamp engines - %s", strings.Join(mod.stampNames[:], " ")),
} }
} }
@@ -266,6 +286,8 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
engines(mod.embedNames), engines(mod.embedNames),
engines(mod.readBookmarksNames), engines(mod.readBookmarksNames),
engines(mod.writeBookmarksNames), engines(mod.writeBookmarksNames),
engines(mod.watermarkNames),
engines(mod.stampNames),
), nil ), nil
} }
@@ -293,6 +315,8 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
writeBookmarksRoute(engine), writeBookmarksRoute(engine),
encryptRoute(engine), encryptRoute(engine),
embedRoute(engine), embedRoute(engine),
watermarkRoute(engine),
stampRoute(engine),
}, nil }, nil
} }

View File

@@ -391,6 +391,122 @@ func EmbedFilesStub(ctx *api.Context, engine gotenberg.PdfEngine, embedPaths []s
return nil 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. // mergeRoute returns an [api.Route] which can merge PDFs.
func mergeRoute(engine gotenberg.PdfEngine) api.Route { func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return api.Route{ return api.Route{
@@ -406,6 +522,10 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
bookmarks := FormDataPdfBookmarks(form, false) bookmarks := FormDataPdfBookmarks(form, false)
userPassword, ownerPassword := FormDataPdfEncrypt(form) userPassword, ownerPassword := FormDataPdfEncrypt(form)
embedPaths := FormDataPdfEmbeds(form) embedPaths := FormDataPdfEmbeds(form)
watermark := FormDataPdfWatermark(form, false)
watermarkFiles := FormDataPdfWatermarkFiles(form)
stamp := FormDataPdfStamp(form, false)
stampFiles := FormDataPdfStampFiles(form)
var inputPaths []string var inputPaths []string
var flatten bool var flatten bool
@@ -419,6 +539,13 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) 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") outputPath := ctx.GeneratePath(".pdf")
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath) err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
if err != nil { if err != nil {
@@ -430,6 +557,16 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("convert PDF: %w", err) 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) err = EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
if err != nil { if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err) return fmt.Errorf("embed files into PDFs: %w", err)
@@ -520,6 +657,10 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
metadata := FormDataPdfMetadata(form, false) metadata := FormDataPdfMetadata(form, false)
userPassword, ownerPassword := FormDataPdfEncrypt(form) userPassword, ownerPassword := FormDataPdfEncrypt(form)
embedPaths := FormDataPdfEmbeds(form) embedPaths := FormDataPdfEmbeds(form)
watermark := FormDataPdfWatermark(form, false)
watermarkFiles := FormDataPdfWatermarkFiles(form)
stamp := FormDataPdfStamp(form, false)
stampFiles := FormDataPdfStampFiles(form)
var inputPaths []string var inputPaths []string
var flatten bool var flatten bool
@@ -531,6 +672,13 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
return fmt.Errorf("validate form data: %w", err) 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) outputPaths, err := SplitPdfStub(ctx, engine, mode, inputPaths)
if err != nil { if err != nil {
return fmt.Errorf("split PDFs: %w", err) 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) 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) err = EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
if err != nil { if err != nil {
return fmt.Errorf("embed files into PDFs: %w", err) 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
},
}
}

View File

@@ -203,6 +203,64 @@ func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *zap.Logger, filePat
return fmt.Errorf("embed files with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("embed files with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Watermark applies a watermark (behind page content) to a PDF file using PDFtk.
// Only PDF source is supported.
func (engine *PdfTk) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
if stamp.Source != gotenberg.StampSourcePDF {
return fmt.Errorf("watermark PDF with PDFtk: %w", gotenberg.ErrPdfStampSourceNotSupported)
}
tmpPath := inputPath + ".tmp"
args := []string{inputPath, "background", stamp.Expression, "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("watermark 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
}
// Stamp applies a stamp (on top of page content) to a PDF file using PDFtk.
// Only PDF source is supported.
func (engine *PdfTk) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
if stamp.Source != gotenberg.StampSourcePDF {
return fmt.Errorf("stamp PDF with PDFtk: %w", gotenberg.ErrPdfStampSourceNotSupported)
}
tmpPath := inputPath + ".tmp"
args := []string{inputPath, "stamp", stamp.Expression, "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("stamp 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)

View File

@@ -221,6 +221,16 @@ func (engine *QPdf) EmbedFiles(ctx context.Context, logger *zap.Logger, filePath
return fmt.Errorf("embed files with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) return fmt.Errorf("embed files with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
} }
// Watermark is not available in this implementation.
func (engine *QPdf) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("watermark PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
// Stamp is not available in this implementation.
func (engine *QPdf) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
return fmt.Errorf("stamp PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
}
var ( var (
_ gotenberg.Module = (*QPdf)(nil) _ gotenberg.Module = (*QPdf)(nil)
_ gotenberg.Provisioner = (*QPdf)(nil) _ gotenberg.Provisioner = (*QPdf)(nil)

View File

@@ -988,6 +988,28 @@ Feature: /forms/chromium/convert/html
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be encrypted Then the response PDF(s) should be encrypted
@watermark
Scenario: POST /forms/chromium/convert/html (Watermark - Text)
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 |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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
@stamp
Scenario: POST /forms/chromium/convert/html (Stamp - Text)
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 |
| stampSource | text | field |
| stampExpression | DRAFT | 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
@@ -1007,9 +1029,11 @@ Feature: /forms/chromium/convert/html
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF. # FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert @convert
@metadata @metadata
@watermark
@stamp
@flatten @flatten
@embed @embed
Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds) Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1 & Metadata & Watermark & Stamp & 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/chromium/convert/html" endpoint with the following form data and header(s): 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 | | files | testdata/page-1-html/index.html | file |
@@ -1025,7 +1049,7 @@ Feature: /forms/chromium/convert/html
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 9 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 11 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded Then the response PDF(s) should have the "embed_1.xml" file embedded

View File

@@ -563,6 +563,62 @@ Feature: /forms/libreoffice/convert
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be encrypted Then the response PDF(s) should be encrypted
@watermark
Scenario: POST /forms/libreoffice/convert (Watermark - Text)
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 |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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
@stamp
Scenario: POST /forms/libreoffice/convert (Stamp - Text)
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 |
| stampSource | text | field |
| stampExpression | DRAFT | 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
Scenario: POST /forms/libreoffice/convert (Native Watermark - Text)
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 |
| nativeWatermarkText | CONFIDENTIAL | 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
Scenario: POST /forms/libreoffice/convert (Native Watermark - Text with Options)
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 |
| nativeWatermarkText | DRAFT | field |
| nativeWatermarkColor | 16711680 | field |
| nativeWatermarkFontHeight | 48 | field |
| nativeWatermarkRotateAngle | 450 | field |
| nativeWatermarkFontName | Courier | 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
Scenario: POST /forms/libreoffice/convert (Native Watermark - Tiled)
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 |
| nativeTiledWatermarkText | CONFIDENTIAL | 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/libreoffice/convert (Embeds) Scenario: POST /forms/libreoffice/convert (Embeds)
Given I have a default Gotenberg container Given I have a default Gotenberg container
@@ -582,15 +638,21 @@ Feature: /forms/libreoffice/convert
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF. # FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert @convert
@metadata @metadata
@watermark
@stamp
@flatten @flatten
@embed @embed
Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds) Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1 & Metadata & Watermark & Stamp & 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 |
| pdfa | PDF/A-1b | field | | pdfa | PDF/A-1b | field |
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
| stampSource | text | field |
| stampExpression | DRAFT | 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 |
@@ -600,8 +662,8 @@ Feature: /forms/libreoffice/convert
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then there should be the following file(s) in the response: Then there should be the following file(s) in the response:
| foo.pdf | | foo.pdf |
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 10 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 12 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 5 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded Then the response PDF(s) should have the "embed_2.xml" file embedded

View File

@@ -401,6 +401,30 @@ Feature: /forms/pdfengines/merge
Then there should be 1 PDF(s) in the response Then there should be 1 PDF(s) in the response
Then the response PDF(s) should be encrypted Then the response PDF(s) should be encrypted
@watermark
Scenario: POST /forms/pdfengines/merge (Watermark - Text)
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 |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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
@stamp
Scenario: POST /forms/pdfengines/merge (Stamp - Text)
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 |
| stampSource | text | field |
| stampExpression | DRAFT | 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
@@ -417,10 +441,12 @@ Feature: /forms/pdfengines/merge
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF. # FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
@convert @convert
@metadata @metadata
@watermark
@stamp
@flatten @flatten
@embed @embed
@bookmarks @bookmarks
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds & Bookmarks) Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Watermark & Stamp & 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 |
@@ -428,6 +454,10 @@ Feature: /forms/pdfengines/merge
| pdfa | PDF/A-1b | field | | pdfa | PDF/A-1b | field |
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
| stampSource | text | field |
| stampExpression | DRAFT | 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 |
@@ -447,8 +477,8 @@ Feature: /forms/pdfengines/merge
""" """
Page 2 Page 2
""" """
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 10 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 12 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 5 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded Then the response PDF(s) should have the "embed_2.xml" file embedded

View File

@@ -473,6 +473,32 @@ Feature: /forms/pdfengines/split
Then there should be 2 PDF(s) in the response Then there should be 2 PDF(s) in the response
Then the response PDF(s) should be encrypted Then the response PDF(s) should be encrypted
@watermark
Scenario: POST /forms/pdfengines/split (Watermark - Text)
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 |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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
@stamp
Scenario: POST /forms/pdfengines/split (Stamp - Text)
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 |
| stampSource | text | field |
| stampExpression | DRAFT | 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
@@ -494,20 +520,26 @@ Feature: /forms/pdfengines/split
# FIXME: once decrypt is done, add encrypt and check after the content of the PDFs. # FIXME: once decrypt is done, add encrypt and check after the content of the PDFs.
@convert @convert
@metadata @metadata
@watermark
@stamp
@flatten @flatten
@embed @embed
Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds) Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1 & Metadata & Watermark & Stamp & 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 |
| splitMode | intervals | field | | splitMode | intervals | field |
| splitSpan | 2 | field | | splitSpan | 2 | field |
| pdfa | PDF/A-1b | field | | pdfa | PDF/A-1b | field |
| pdfua | true | field | | pdfua | true | field |
| metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field | | metadata | {"Author":"Julien Neuhart","Copyright":"Julien Neuhart","CreateDate":"2006-09-18T16:27:50-04:00","Creator":"Gotenberg","Keywords":["first","second"],"Marked":true,"ModDate":"2006-09-18T16:27:50-04:00","PDFVersion":1.7,"Producer":"Gotenberg","Subject":"Sample","Title":"Sample","Trapped":"Unknown"} | field |
| flatten | true | field | | watermarkSource | text | field |
| embeds | testdata/embed_1.xml | file | | watermarkExpression | CONFIDENTIAL | field |
| embeds | testdata/embed_2.xml | file | | stampSource | text | field |
| stampExpression | DRAFT | field |
| flatten | true | field |
| embeds | testdata/embed_1.xml | file |
| embeds | testdata/embed_2.xml | file |
Then the response status code should be 200 Then the response status code should be 200
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
@@ -528,8 +560,8 @@ Feature: /forms/pdfengines/split
""" """
Page 3 Page 3
""" """
Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 10 failed rule(s) Then the response PDF(s) should be valid "PDF/A-1b" with a tolerance of 12 failed rule(s)
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 2 failed rule(s) Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 5 failed rule(s)
Then the response PDF(s) should be flatten Then the response PDF(s) should be flatten
Then the response PDF(s) should have the "embed_1.xml" file embedded Then the response PDF(s) should have the "embed_1.xml" file embedded
Then the response PDF(s) should have the "embed_2.xml" file embedded Then the response PDF(s) should have the "embed_2.xml" file embedded

View File

@@ -0,0 +1,211 @@
@pdfengines
@pdfengines-stamp
@stamp
Feature: /forms/pdfengines/stamp
Scenario: POST /forms/pdfengines/stamp (Text - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | 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/stamp (Text with Pages - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| stampSource | text | field |
| stampExpression | DRAFT | field |
| stampPages | 1-2 | 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/stamp (Text with Options - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | SAMPLE | field |
| stampOptions | {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"} | 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
Scenario: POST /forms/pdfengines/stamp (Image - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stamps | testdata/watermark.png | file |
| stampSource | image | 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
Scenario: POST /forms/pdfengines/stamp (PDF - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stamps | testdata/page_2.pdf | file |
| stampSource | pdf | 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
Scenario: POST /forms/pdfengines/stamp (PDF - pdftk)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdftk |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stamps | testdata/page_2.pdf | file |
| stampSource | pdf | 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
Scenario: POST /forms/pdfengines/stamp (Text - pdftk unsupported)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_STAMP_ENGINES | pdftk |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | field |
Then the response status code should be 400
Then the response body should match string:
"""
At least one PDF engine cannot process the requested stamp source type, while others may have failed due to different issues
"""
Scenario: POST /forms/pdfengines/stamp (Many PDFs)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| stampSource | text | field |
| stampExpression | DRAFT | 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/stamp (Bad Request - No Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" 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 'stampSource' is required
"""
Scenario: POST /forms/pdfengines/stamp (Bad Request - Invalid Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | foo | field |
Then the response status code should be 400
Then the response body should contain string:
"""
Invalid form data: form field 'stampSource' is invalid
"""
Scenario: POST /forms/pdfengines/stamp (Bad Request - Missing File for Image Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | image | field |
Then the response status code should be 400
Then the response body should match string:
"""
Invalid form data: a stamp file is required for image or pdf source
"""
Scenario: POST /forms/pdfengines/stamp (Bad Request - Missing File for PDF Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | pdf | field |
Then the response status code should be 400
Then the response body should match string:
"""
Invalid form data: a stamp file is required for image or pdf source
"""
Scenario: POST /forms/pdfengines/stamp (Bad Request - No PDF)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | 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/stamp (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/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | field |
Then the response status code should be 404
Scenario: POST /forms/pdfengines/stamp (Gotenberg Trace)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | field |
| Gotenberg-Trace | forms_pdfengines_stamp | 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_stamp"
Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_stamp" |
@webhook
Scenario: POST /forms/pdfengines/stamp (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/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | 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/stamp (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/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | field |
Then the response status code should be 401
Scenario: POST /foo/forms/pdfengines/stamp (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/stamp" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| stampSource | text | field |
| stampExpression | CONFIDENTIAL | field |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"

View File

@@ -0,0 +1,211 @@
@pdfengines
@pdfengines-watermark
@watermark
Feature: /forms/pdfengines/watermark
Scenario: POST /forms/pdfengines/watermark (Text - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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/watermark (Text with Pages - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/pages_3.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | DRAFT | field |
| watermarkPages | 1-2 | 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/watermark (Text with Options - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | SAMPLE | field |
| watermarkOptions | {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"} | 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
Scenario: POST /forms/pdfengines/watermark (Image - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarks | testdata/watermark.png | file |
| watermarkSource | image | 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
Scenario: POST /forms/pdfengines/watermark (PDF - pdfcpu)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarks | testdata/page_2.pdf | file |
| watermarkSource | pdf | 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
Scenario: POST /forms/pdfengines/watermark (PDF - pdftk)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdftk |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarks | testdata/page_2.pdf | file |
| watermarkSource | pdf | 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
Scenario: POST /forms/pdfengines/watermark (Text - pdftk unsupported)
Given I have a Gotenberg container with the following environment variable(s):
| PDFENGINES_WATERMARK_ENGINES | pdftk |
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
Then the response status code should be 400
Then the response body should match string:
"""
At least one PDF engine cannot process the requested stamp source type, while others may have failed due to different issues
"""
Scenario: POST /forms/pdfengines/watermark (Many PDFs)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| files | testdata/page_2.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | DRAFT | 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/watermark (Bad Request - No Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" 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 'watermarkSource' is required
"""
Scenario: POST /forms/pdfengines/watermark (Bad Request - Invalid Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | foo | field |
Then the response status code should be 400
Then the response body should contain string:
"""
Invalid form data: form field 'watermarkSource' is invalid
"""
Scenario: POST /forms/pdfengines/watermark (Bad Request - Missing File for Image Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | image | field |
Then the response status code should be 400
Then the response body should match string:
"""
Invalid form data: a watermark file is required for image or pdf source
"""
Scenario: POST /forms/pdfengines/watermark (Bad Request - Missing File for PDF Source)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | pdf | field |
Then the response status code should be 400
Then the response body should match string:
"""
Invalid form data: a watermark file is required for image or pdf source
"""
Scenario: POST /forms/pdfengines/watermark (Bad Request - No PDF)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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/watermark (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/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
Then the response status code should be 404
Scenario: POST /forms/pdfengines/watermark (Gotenberg Trace)
Given I have a default Gotenberg container
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
| Gotenberg-Trace | forms_pdfengines_watermark | 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_watermark"
Then the Gotenberg container should log the following entries:
| "trace":"forms_pdfengines_watermark" |
@webhook
Scenario: POST /forms/pdfengines/watermark (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/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | 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/watermark (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/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
Then the response status code should be 401
Scenario: POST /foo/forms/pdfengines/watermark (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/watermark" endpoint with the following form data and header(s):
| files | testdata/page_1.pdf | file |
| watermarkSource | text | field |
| watermarkExpression | CONFIDENTIAL | field |
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"

BIN
test/integration/testdata/watermark.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 B