mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-08 08:32:16 +01:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b0eb06991 | ||
|
|
410c1dfd7f | ||
|
|
b98378311f | ||
|
|
e6306e7c2f | ||
|
|
0663e5f92b | ||
|
|
21e300fcec | ||
|
|
7fb4c89832 | ||
|
|
1e26fdd35b | ||
|
|
19db80bc2e | ||
|
|
4ac493250c | ||
|
|
ec9d960538 | ||
|
|
5fa6603fa4 | ||
|
|
d81517bf26 | ||
|
|
9af0f2896c | ||
|
|
f619ce09d6 | ||
|
|
58c41dd01f | ||
|
|
ec3e415bf0 | ||
|
|
230cb8da2c | ||
|
|
59f96358c4 | ||
|
|
874e78c6cd | ||
|
|
caea81501d | ||
|
|
1578253fb1 | ||
|
|
fec6437c5d | ||
|
|
edff24913a | ||
|
|
5120b49639 |
72
.bruno/Chromium/Convert/HTML to PDF.bru
Normal file
72
.bruno/Chromium/Convert/HTML to PDF.bru
Normal file
@@ -0,0 +1,72 @@
|
||||
meta {
|
||||
name: HTML to PDF
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/chromium/convert/html
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page-1-html/index.html)
|
||||
~landscape: false
|
||||
~printBackground: false
|
||||
~scale: 1.0
|
||||
~singlePage: false
|
||||
~paperWidth: 8.5
|
||||
~paperHeight: 11
|
||||
~marginTop: 0.39
|
||||
~marginBottom: 0.39
|
||||
~marginLeft: 0.39
|
||||
~marginRight: 0.39
|
||||
~nativePageRanges: 1-2
|
||||
~preferCssPageSize: false
|
||||
~generateDocumentOutline: false
|
||||
~generateTaggedPdf: false
|
||||
~skipNetworkIdleEvent: false
|
||||
~failOnHttpStatusCodes: [499,599]
|
||||
~failOnResourceHttpStatusCodes: []
|
||||
~ignoreResourceHttpStatusDomains: []
|
||||
~failOnResourceLoadingFailed: false
|
||||
~failOnConsoleExceptions: false
|
||||
~waitDelay: 0s
|
||||
~waitWindowStatus:
|
||||
~waitForExpression:
|
||||
~waitForSelector:
|
||||
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||
~userAgent:
|
||||
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||
~emulatedMediaType: print
|
||||
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||
~omitBackground: false
|
||||
~splitMode: intervals
|
||||
~splitSpan: 1
|
||||
~splitUnify: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||
~userPassword:
|
||||
~ownerPassword:
|
||||
~watermarkSource: text
|
||||
~watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages:
|
||||
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||
~stampSource: text
|
||||
~stampExpression: DRAFT
|
||||
~stampPages:
|
||||
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||
~rotateAngle: 90
|
||||
~rotatePages:
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-file
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
73
.bruno/Chromium/Convert/Markdown to PDF.bru
Normal file
73
.bruno/Chromium/Convert/Markdown to PDF.bru
Normal file
@@ -0,0 +1,73 @@
|
||||
meta {
|
||||
name: Markdown to PDF
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/chromium/convert/markdown
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page-1-markdown/index.html)
|
||||
files: @file(../../test/integration/testdata/page-1-markdown/page_1.md)
|
||||
~landscape: false
|
||||
~printBackground: false
|
||||
~scale: 1.0
|
||||
~singlePage: false
|
||||
~paperWidth: 8.5
|
||||
~paperHeight: 11
|
||||
~marginTop: 0.39
|
||||
~marginBottom: 0.39
|
||||
~marginLeft: 0.39
|
||||
~marginRight: 0.39
|
||||
~nativePageRanges: 1-2
|
||||
~preferCssPageSize: false
|
||||
~generateDocumentOutline: false
|
||||
~generateTaggedPdf: false
|
||||
~skipNetworkIdleEvent: false
|
||||
~failOnHttpStatusCodes: [499,599]
|
||||
~failOnResourceHttpStatusCodes: []
|
||||
~ignoreResourceHttpStatusDomains: []
|
||||
~failOnResourceLoadingFailed: false
|
||||
~failOnConsoleExceptions: false
|
||||
~waitDelay: 0s
|
||||
~waitWindowStatus:
|
||||
~waitForExpression:
|
||||
~waitForSelector:
|
||||
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||
~userAgent:
|
||||
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||
~emulatedMediaType: print
|
||||
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||
~omitBackground: false
|
||||
~splitMode: intervals
|
||||
~splitSpan: 1
|
||||
~splitUnify: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||
~userPassword:
|
||||
~ownerPassword:
|
||||
~watermarkSource: text
|
||||
~watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages:
|
||||
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||
~stampSource: text
|
||||
~stampExpression: DRAFT
|
||||
~stampPages:
|
||||
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||
~rotateAngle: 90
|
||||
~rotatePages:
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-file
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
72
.bruno/Chromium/Convert/URL to PDF.bru
Normal file
72
.bruno/Chromium/Convert/URL to PDF.bru
Normal file
@@ -0,0 +1,72 @@
|
||||
meta {
|
||||
name: URL to PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/chromium/convert/url
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
url: https://example.com
|
||||
~landscape: false
|
||||
~printBackground: false
|
||||
~scale: 1.0
|
||||
~singlePage: false
|
||||
~paperWidth: 8.5
|
||||
~paperHeight: 11
|
||||
~marginTop: 0.39
|
||||
~marginBottom: 0.39
|
||||
~marginLeft: 0.39
|
||||
~marginRight: 0.39
|
||||
~nativePageRanges: 1-2
|
||||
~preferCssPageSize: false
|
||||
~generateDocumentOutline: false
|
||||
~generateTaggedPdf: false
|
||||
~skipNetworkIdleEvent: false
|
||||
~failOnHttpStatusCodes: [499,599]
|
||||
~failOnResourceHttpStatusCodes: []
|
||||
~ignoreResourceHttpStatusDomains: []
|
||||
~failOnResourceLoadingFailed: false
|
||||
~failOnConsoleExceptions: false
|
||||
~waitDelay: 0s
|
||||
~waitWindowStatus:
|
||||
~waitForExpression:
|
||||
~waitForSelector:
|
||||
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||
~userAgent:
|
||||
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||
~emulatedMediaType: print
|
||||
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||
~omitBackground: false
|
||||
~splitMode: intervals
|
||||
~splitSpan: 1
|
||||
~splitUnify: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||
~userPassword:
|
||||
~ownerPassword:
|
||||
~watermarkSource: text
|
||||
~watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages:
|
||||
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||
~stampSource: text
|
||||
~stampExpression: DRAFT
|
||||
~stampPages:
|
||||
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||
~rotateAngle: 90
|
||||
~rotatePages:
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-file
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
46
.bruno/Chromium/Screenshot/HTML Screenshot.bru
Normal file
46
.bruno/Chromium/Screenshot/HTML Screenshot.bru
Normal file
@@ -0,0 +1,46 @@
|
||||
meta {
|
||||
name: HTML Screenshot
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/chromium/screenshot/html
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page-1-html/index.html)
|
||||
~width: 800
|
||||
~height: 600
|
||||
~clip: false
|
||||
~format: png
|
||||
~quality: 100
|
||||
~optimizeForSpeed: false
|
||||
~skipNetworkIdleEvent: false
|
||||
~failOnHttpStatusCodes: [499,599]
|
||||
~failOnResourceHttpStatusCodes: []
|
||||
~ignoreResourceHttpStatusDomains: []
|
||||
~failOnResourceLoadingFailed: false
|
||||
~failOnConsoleExceptions: false
|
||||
~waitDelay: 0s
|
||||
~waitWindowStatus:
|
||||
~waitForExpression:
|
||||
~waitForSelector:
|
||||
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||
~userAgent:
|
||||
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||
~emulatedMediaType: screen
|
||||
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||
~omitBackground: false
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-screenshot
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
47
.bruno/Chromium/Screenshot/Markdown Screenshot.bru
Normal file
47
.bruno/Chromium/Screenshot/Markdown Screenshot.bru
Normal file
@@ -0,0 +1,47 @@
|
||||
meta {
|
||||
name: Markdown Screenshot
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/chromium/screenshot/markdown
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page-1-markdown/index.html)
|
||||
files: @file(../../test/integration/testdata/page-1-markdown/page_1.md)
|
||||
~width: 800
|
||||
~height: 600
|
||||
~clip: false
|
||||
~format: png
|
||||
~quality: 100
|
||||
~optimizeForSpeed: false
|
||||
~skipNetworkIdleEvent: false
|
||||
~failOnHttpStatusCodes: [499,599]
|
||||
~failOnResourceHttpStatusCodes: []
|
||||
~ignoreResourceHttpStatusDomains: []
|
||||
~failOnResourceLoadingFailed: false
|
||||
~failOnConsoleExceptions: false
|
||||
~waitDelay: 0s
|
||||
~waitWindowStatus:
|
||||
~waitForExpression:
|
||||
~waitForSelector:
|
||||
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||
~userAgent:
|
||||
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||
~emulatedMediaType: screen
|
||||
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||
~omitBackground: false
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-screenshot
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
46
.bruno/Chromium/Screenshot/URL Screenshot.bru
Normal file
46
.bruno/Chromium/Screenshot/URL Screenshot.bru
Normal file
@@ -0,0 +1,46 @@
|
||||
meta {
|
||||
name: URL Screenshot
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/chromium/screenshot/url
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
url: https://example.com
|
||||
~width: 800
|
||||
~height: 600
|
||||
~clip: false
|
||||
~format: png
|
||||
~quality: 100
|
||||
~optimizeForSpeed: false
|
||||
~skipNetworkIdleEvent: false
|
||||
~failOnHttpStatusCodes: [499,599]
|
||||
~failOnResourceHttpStatusCodes: []
|
||||
~ignoreResourceHttpStatusDomains: []
|
||||
~failOnResourceLoadingFailed: false
|
||||
~failOnConsoleExceptions: false
|
||||
~waitDelay: 0s
|
||||
~waitWindowStatus:
|
||||
~waitForExpression:
|
||||
~waitForSelector:
|
||||
~cookies: [{"name":"my_cookie","value":"my_value","domain":"example.com"}]
|
||||
~userAgent:
|
||||
~extraHttpHeaders: {"X-Custom-Header":"value"}
|
||||
~emulatedMediaType: screen
|
||||
~emulatedMediaFeatures: {"prefers-color-scheme":"dark"}
|
||||
~omitBackground: false
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-screenshot
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
11
.bruno/Health & Info/Debug.bru
Normal file
11
.bruno/Health & Info/Debug.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Debug
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/debug
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
.bruno/Health & Info/Health.bru
Normal file
11
.bruno/Health & Info/Health.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Health
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/health
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
.bruno/Health & Info/Prometheus Metrics.bru
Normal file
11
.bruno/Health & Info/Prometheus Metrics.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Prometheus Metrics
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/prometheus/metrics
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
11
.bruno/Health & Info/Version.bru
Normal file
11
.bruno/Health & Info/Version.bru
Normal file
@@ -0,0 +1,11 @@
|
||||
meta {
|
||||
name: Version
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{baseUrl}}/version
|
||||
body: none
|
||||
auth: none
|
||||
}
|
||||
74
.bruno/LibreOffice/Convert to PDF.bru
Normal file
74
.bruno/LibreOffice/Convert to PDF.bru
Normal file
@@ -0,0 +1,74 @@
|
||||
meta {
|
||||
name: Convert to PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/libreoffice/convert
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.docx)
|
||||
~password:
|
||||
~landscape: false
|
||||
~nativePageRanges:
|
||||
~updateIndexes: false
|
||||
~exportFormFields: true
|
||||
~allowDuplicateFieldNames: false
|
||||
~exportBookmarks: true
|
||||
~exportBookmarksToPdfDestination: false
|
||||
~exportPlaceholders: false
|
||||
~exportNotes: false
|
||||
~exportNotesPages: false
|
||||
~exportOnlyNotesPages: false
|
||||
~exportNotesInMargin: false
|
||||
~convertOooTargetToPdfTarget: false
|
||||
~exportLinksRelativeFsys: false
|
||||
~exportHiddenSlides: false
|
||||
~skipEmptyPages: false
|
||||
~addOriginalDocumentAsStream: false
|
||||
~singlePageSheets: false
|
||||
~losslessImageCompression: false
|
||||
~quality: 90
|
||||
~reduceImageResolution: false
|
||||
~maxImageResolution: 300
|
||||
~nativeWatermarkText:
|
||||
~nativeWatermarkColor: 0
|
||||
~nativeWatermarkFontHeight: 0
|
||||
~nativeWatermarkRotateAngle: 0
|
||||
~nativeWatermarkFontName:
|
||||
~nativeTiledWatermarkText:
|
||||
~nativePdfFormats: true
|
||||
~merge: false
|
||||
~flatten: false
|
||||
~splitMode: intervals
|
||||
~splitSpan: 1
|
||||
~splitUnify: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||
~userPassword:
|
||||
~ownerPassword:
|
||||
~watermarkSource: text
|
||||
~watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages:
|
||||
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||
~stampSource: text
|
||||
~stampExpression: DRAFT
|
||||
~stampPages:
|
||||
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||
~rotateAngle: 90
|
||||
~rotatePages:
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: my-file
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
23
.bruno/PDF Engines/Bookmarks/Read Bookmarks.bru
Normal file
23
.bruno/PDF Engines/Bookmarks/Read Bookmarks.bru
Normal file
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Read Bookmarks
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/bookmarks/read
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
25
.bruno/PDF Engines/Bookmarks/Write Bookmarks.bru
Normal file
25
.bruno/PDF Engines/Bookmarks/Write Bookmarks.bru
Normal file
@@ -0,0 +1,25 @@
|
||||
meta {
|
||||
name: Write Bookmarks
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/bookmarks/write
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
bookmarks: [{"title":"Chapter 1","page":1,"children":[{"title":"Section 1.1","page":1}]}]
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: with-bookmarks
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
26
.bruno/PDF Engines/Convert/Convert PDF.bru
Normal file
26
.bruno/PDF Engines/Convert/Convert PDF.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Convert PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/convert
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: converted
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
26
.bruno/PDF Engines/Embed/Embed Files.bru
Normal file
26
.bruno/PDF Engines/Embed/Embed Files.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Embed Files
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/embed
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
embeds: @file(../../test/integration/testdata/page_1.pdf)
|
||||
~downloadFrom: [{"url":"https://example.com/attachment.xml","embedded":true}]
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: with-embeds
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
26
.bruno/PDF Engines/Encrypt/Encrypt PDF.bru
Normal file
26
.bruno/PDF Engines/Encrypt/Encrypt PDF.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Encrypt PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/encrypt
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
userPassword: secret123
|
||||
~ownerPassword: owner456
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: encrypted
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
24
.bruno/PDF Engines/Flatten/Flatten PDF.bru
Normal file
24
.bruno/PDF Engines/Flatten/Flatten PDF.bru
Normal file
@@ -0,0 +1,24 @@
|
||||
meta {
|
||||
name: Flatten PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/flatten
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: flattened
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
43
.bruno/PDF Engines/Merge/Merge PDFs.bru
Normal file
43
.bruno/PDF Engines/Merge/Merge PDFs.bru
Normal file
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: Merge PDFs
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/merge
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
files: @file(../../test/integration/testdata/page_2.pdf)
|
||||
~flatten: false
|
||||
~autoIndexBookmarks: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||
~bookmarks: [{"title":"Page 1","page":1},{"title":"Page 2","page":2}]
|
||||
~userPassword:
|
||||
~ownerPassword:
|
||||
~watermarkSource: text
|
||||
~watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages:
|
||||
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||
~stampSource: text
|
||||
~stampExpression: DRAFT
|
||||
~stampPages:
|
||||
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||
~rotateAngle: 90
|
||||
~rotatePages:
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: merged
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
23
.bruno/PDF Engines/Metadata/Read Metadata.bru
Normal file
23
.bruno/PDF Engines/Metadata/Read Metadata.bru
Normal file
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Read Metadata
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/metadata/read
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
25
.bruno/PDF Engines/Metadata/Write Metadata.bru
Normal file
25
.bruno/PDF Engines/Metadata/Write Metadata.bru
Normal file
@@ -0,0 +1,25 @@
|
||||
meta {
|
||||
name: Write Metadata
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/metadata/write
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
metadata: {"Author":"Bruno","Copyright":"Bruno","Creator":"Gotenberg","Keywords":["test"],"Marked":true,"Producer":"Gotenberg","Subject":"Test","Title":"Test Document","Trapped":"Unknown"}
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: with-metadata
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
26
.bruno/PDF Engines/Rotate/Rotate PDF.bru
Normal file
26
.bruno/PDF Engines/Rotate/Rotate PDF.bru
Normal file
@@ -0,0 +1,26 @@
|
||||
meta {
|
||||
name: Rotate PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/rotate
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
rotateAngle: 90
|
||||
~rotatePages: 1-2
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: rotated
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
43
.bruno/PDF Engines/Split/Split PDF.bru
Normal file
43
.bruno/PDF Engines/Split/Split PDF.bru
Normal file
@@ -0,0 +1,43 @@
|
||||
meta {
|
||||
name: Split PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/split
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/pages_3.pdf)
|
||||
splitMode: intervals
|
||||
splitSpan: 1
|
||||
~splitUnify: false
|
||||
~flatten: false
|
||||
~pdfa: PDF/A-1b
|
||||
~pdfua: true
|
||||
~metadata: {"Author":"Bruno","Title":"Test"}
|
||||
~userPassword:
|
||||
~ownerPassword:
|
||||
~watermarkSource: text
|
||||
~watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages:
|
||||
~watermarkOptions: {"scale":"0.5 abs","rot":"45","fillcolor":"#FF0000"}
|
||||
~stampSource: text
|
||||
~stampExpression: DRAFT
|
||||
~stampPages:
|
||||
~stampOptions: {"scale":"0.5 abs","rot":"45"}
|
||||
~rotateAngle: 90
|
||||
~rotatePages:
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: split
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
30
.bruno/PDF Engines/Stamp/Stamp PDF.bru
Normal file
30
.bruno/PDF Engines/Stamp/Stamp PDF.bru
Normal file
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: Stamp PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/stamp
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
stampSource: text
|
||||
stampExpression: APPROVED
|
||||
~stampPages: 1-2
|
||||
~stampOptions: {"font":"Helvetica","fontSize":"48","color":"#00FF00","opacity":"0.5","rotation":"0"}
|
||||
~stamp: @file(../../test/integration/testdata/watermark.png)
|
||||
~downloadFrom: [{"url":"https://example.com/stamp.png","field":"stamp"}]
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: stamped
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
30
.bruno/PDF Engines/Watermark/Watermark PDF.bru
Normal file
30
.bruno/PDF Engines/Watermark/Watermark PDF.bru
Normal file
@@ -0,0 +1,30 @@
|
||||
meta {
|
||||
name: Watermark PDF
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/pdfengines/watermark
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/page_1.pdf)
|
||||
watermarkSource: text
|
||||
watermarkExpression: CONFIDENTIAL
|
||||
~watermarkPages: 1-2
|
||||
~watermarkOptions: {"font":"Helvetica","fontSize":"48","color":"#FF0000","opacity":"0.3","rotation":"45"}
|
||||
~watermark: @file(../../test/integration/testdata/watermark.png)
|
||||
~downloadFrom: [{"url":"https://example.com/watermark.png","field":"watermark"}]
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: watermarked
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
6
.bruno/bruno.json
Normal file
6
.bruno/bruno.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "Gotenberg",
|
||||
"type": "collection",
|
||||
"ignore": ["node_modules", ".git"]
|
||||
}
|
||||
3
.bruno/collection.bru
Normal file
3
.bruno/collection.bru
Normal file
@@ -0,0 +1,3 @@
|
||||
headers {
|
||||
Gotenberg-Trace: {{traceId}}
|
||||
}
|
||||
4
.bruno/environments/Demo.bru
Normal file
4
.bruno/environments/Demo.bru
Normal file
@@ -0,0 +1,4 @@
|
||||
vars {
|
||||
baseUrl: https://demo.gotenberg.dev
|
||||
traceId: bruno-demo
|
||||
}
|
||||
4
.bruno/environments/Local.bru
Normal file
4
.bruno/environments/Local.bru
Normal file
@@ -0,0 +1,4 @@
|
||||
vars {
|
||||
baseUrl: http://localhost:3000
|
||||
traceId: bruno-local
|
||||
}
|
||||
33
.github/actions/build-test-push/action.yml
vendored
33
.github/actions/build-test-push/action.yml
vendored
@@ -60,30 +60,43 @@ runs:
|
||||
- name: Build ${{ inputs.platform }}
|
||||
id: build
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
INPUT_PLATFORM: ${{ inputs.platform }}
|
||||
INPUT_ALTERNATE_REPOSITORY: ${{ inputs.alternate_repository }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
.github/actions/build-test-push/build.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
--version "$INPUT_VERSION" \
|
||||
--platform "$INPUT_PLATFORM" \
|
||||
--alternate-repository "$INPUT_ALTERNATE_REPOSITORY" \
|
||||
--dry-run "$INPUT_DRY_RUN"
|
||||
|
||||
- name: Run integration tests
|
||||
if: inputs.skip_integrations_tests != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
INPUT_PLATFORM: ${{ inputs.platform }}
|
||||
INPUT_ALTERNATE_REPOSITORY: ${{ inputs.alternate_repository }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
.github/actions/build-test-push/test.sh \
|
||||
--version "${{ inputs.version }}" \
|
||||
--platform "${{ inputs.platform }}" \
|
||||
--alternate-repository "${{ inputs.alternate_repository }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
--version "$INPUT_VERSION" \
|
||||
--platform "$INPUT_PLATFORM" \
|
||||
--alternate-repository "$INPUT_ALTERNATE_REPOSITORY" \
|
||||
--dry-run "$INPUT_DRY_RUN"
|
||||
|
||||
- name: Push
|
||||
if: inputs.docker_hub_username != ''
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_TAGS: ${{ steps.build.outputs.tags }},${{ steps.build.outputs.tags_cloud_run }},${{ steps.build.outputs.tags_aws_lambda }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
.github/actions/build-test-push/push.sh \
|
||||
--tags "${{ steps.build.outputs.tags }},${{ steps.build.outputs.tags_cloud_run }},${{ steps.build.outputs.tags_aws_lambda }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
--tags "$INPUT_TAGS" \
|
||||
--dry-run "$INPUT_DRY_RUN"
|
||||
|
||||
- name: Outputs
|
||||
shell: bash
|
||||
|
||||
9
.github/actions/clean/action.yml
vendored
9
.github/actions/clean/action.yml
vendored
@@ -23,9 +23,12 @@ runs:
|
||||
env:
|
||||
DOCKERHUB_USERNAME: ${{ inputs.docker_hub_username }}
|
||||
DOCKERHUB_TOKEN: ${{ inputs.docker_hub_password }}
|
||||
INPUT_TAGS: ${{ inputs.tags }}
|
||||
INPUT_SNAPSHOT_VERSION: ${{ inputs.snapshot_version }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/clean/clean.sh \
|
||||
--tags "${{ inputs.tags }}" \
|
||||
--snapshot-version "${{ inputs.snapshot_version }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
--tags "$INPUT_TAGS" \
|
||||
--snapshot-version "$INPUT_SNAPSHOT_VERSION" \
|
||||
--dry-run "$INPUT_DRY_RUN"
|
||||
|
||||
10
.github/actions/merge/action.yml
vendored
10
.github/actions/merge/action.yml
vendored
@@ -41,8 +41,12 @@ runs:
|
||||
|
||||
- name: Merge
|
||||
shell: bash
|
||||
env:
|
||||
INPUT_TAGS: ${{ inputs.tags }}
|
||||
INPUT_ALTERNATE_REGISTRY: ${{ inputs.alternate_registry }}
|
||||
INPUT_DRY_RUN: ${{ inputs.dry_run }}
|
||||
run: |
|
||||
.github/actions/merge/merge.sh \
|
||||
--tags "${{ inputs.tags }}" \
|
||||
--alternate-registry "${{ inputs.alternate_registry }}" \
|
||||
--dry-run "${{ inputs.dry_run }}"
|
||||
--tags "$INPUT_TAGS" \
|
||||
--alternate-registry "$INPUT_ALTERNATE_REGISTRY" \
|
||||
--dry-run "$INPUT_DRY_RUN"
|
||||
|
||||
368
AGENTS.md
368
AGENTS.md
@@ -1,66 +1,336 @@
|
||||
# Operational Guidelines for Gotenberg
|
||||
|
||||
As an AI agent working on the Gotenberg repository, you are expected to act with the diligence and architectural foresight of a Senior Go Engineer. Gotenberg is a widely used production dependency; stability and backward compatibility are paramount.
|
||||
You are working on **Gotenberg**, a Docker-based API for converting documents to PDF. It is a widely used production dependency. Stability and backward compatibility are paramount. When in doubt about whether a change is breaking, flag it rather than assuming it's safe.
|
||||
|
||||
## 1. Core Philosophy & Stability
|
||||
## Mandatory Workflow
|
||||
|
||||
- **Backward Compatibility is Law:** This project creates a public API. Never modify existing flags, configuration environment variables, or API form fields unless explicitly instructed to perform a breaking change. If a change is breaking, it must be flagged immediately in the plan.
|
||||
- **Defensive Programming:** Assume input data is malformed. Handle errors explicitly. Do not panic.
|
||||
- **Atomic Commits:** Isolate refactoring from feature additions. A Pull Request should do one thing well.
|
||||
Every task MUST follow these five steps in order. Do not skip any step.
|
||||
|
||||
## 2. Development Workflow & Tooling
|
||||
### Step 1 — Plan
|
||||
|
||||
You must rely strictly on the project's Makefile for build and verification tasks. Do not run `go` commands directly unless debugging a specific package requires it.
|
||||
Before writing any code, produce a plan that covers:
|
||||
|
||||
- **Formatting:** Run `make fmt` to format Go code before committing.
|
||||
- **Linting:**
|
||||
- Run `make lint` to ensure Go code strictly adheres to the `.golangci.yml` configuration.
|
||||
- Run `make lint-prettier` to verify formatting for non-Go files (Markdown, YAML, etc.).
|
||||
- Zero linting errors are permitted.
|
||||
- **Building:** Run `make build` to verify compilation and Docker image construction.
|
||||
- **Problem statement**: What needs to change and why.
|
||||
- **Proposed solution**: The recommended approach with enough detail to implement (files to modify, interface changes, pipeline positioning, form fields, etc.).
|
||||
- **Alternatives considered**: At least one alternative approach when pertinent, with a brief explanation of why the proposed solution is preferred.
|
||||
- **Scope**: List every file that will be created or modified.
|
||||
- **Testing strategy**: Which integration test tags will be affected, what new scenarios are needed, and whether unit tests are required.
|
||||
|
||||
## 3. Architecture & Code Structure
|
||||
Present the plan to the user and wait for approval before proceeding to Step 2. If the user provides a plan, validate it against the codebase and flag any issues before implementing.
|
||||
|
||||
- **Idiomatic Go:** Follow "Effective Go" principles.
|
||||
- **Directory Separation:**
|
||||
- `cmd/`: Application entry points only. Contains wiring and startup logic. **No business logic is permitted here.**
|
||||
- `pkg/`: Core library code and modules. All business logic resides here.
|
||||
- **Module System:** Gotenberg is modular (e.g., Chromium, LibreOffice). When adding features, determine if they belong to an existing module or require a new strict isolation.
|
||||
### Step 2 — Implement
|
||||
|
||||
## 4. Testing Standards
|
||||
Implement the approved plan following the coding standards and patterns described in this document. After implementation, verify the build compiles (`go build ./...`).
|
||||
|
||||
Gotenberg utilizes a split testing strategy. **Integration tests are the primary and preferred method for verifying features.**
|
||||
### Step 3 — Test
|
||||
|
||||
- **Integration Tests (`make test-integration`):**
|
||||
- **First Priority:** Always start here when adding features or routes.
|
||||
- Gotenberg uses **Gherkin (Godog)** for end-to-end verification.
|
||||
- You **must** create or update the corresponding `.feature` file in `test/integration`.
|
||||
- These tests run within the Docker context; ensure environment consistency.
|
||||
- **Unit Tests (`make test-unit`):**
|
||||
- Use table-driven tests for pure logic within `pkg/`.
|
||||
- Mock external dependencies (filesystem, network) where appropriate.
|
||||
Write or update tests based on the plan's testing strategy:
|
||||
|
||||
## 5. Documentation Requirements
|
||||
- **Integration tests** (primary): Gherkin scenarios in `test/integration/features/`. See the [Integration Tests](#integration-tests) section.
|
||||
- **Unit tests** (when applicable): Table-driven tests in `*_test.go` files using mocks from `pkg/gotenberg/mocks.go`.
|
||||
|
||||
- **No README Updates:** Do not modify the root `README.md` unless explicitly asked.
|
||||
- **GoDoc is Mandatory:**
|
||||
- **New Packages:** If creating a new package, you must include a `doc.go` file containing the package-level documentation.
|
||||
- **Exported Symbols:** Every exported function, type, constant, and variable must have a proper GoDoc comment starting with its name.
|
||||
- **Quality:** Comments must be complete sentences explaining _what_ the symbol does and _how_ to use it.
|
||||
- **Example:**
|
||||
```go
|
||||
// Convert transforms the input document to PDF using the Chromium engine.
|
||||
// It returns an error if the connection to the browser instance fails.
|
||||
func Convert(...) error
|
||||
```
|
||||
### Step 4 — Review
|
||||
|
||||
## 6. Definition of Done
|
||||
Self-review the implementation against the [Review Checklist](#review-checklist). Fix any issues found before presenting the result to the user.
|
||||
|
||||
A task is considered complete only when:
|
||||
### Step 5 — Commit
|
||||
|
||||
1. The code compiles via `make build`.
|
||||
2. The code is formatted via `make fmt`.
|
||||
3. All linters pass via `make lint` and `make lint-prettier`.
|
||||
4. Integration scenarios pass via `make test-integration`.
|
||||
5. Unit tests pass via `make test-unit`.
|
||||
6. All exported symbols and new packages have compliant GoDoc.
|
||||
Present the review to the user and **wait for explicit approval**. Do NOT commit until the user confirms. Once approved, create a commit following the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
Stage only the files related to the change. Do not use `git add -A` or `git add .`.
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Backward compatibility is law.** Never modify existing CLI flags, environment variables, or API form fields unless explicitly instructed to perform a breaking change. Flag any breaking change immediately.
|
||||
- **Defensive programming.** Assume input is malformed. Handle errors explicitly. Never panic.
|
||||
- **Atomic commits.** One feature or fix per PR. Isolate refactoring from feature work.
|
||||
- **Idiomatic Go.** Follow "Effective Go" principles. All exported symbols must have GoDoc comments starting with their name.
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
cmd/gotenberg/ → Entry point only (wiring/startup). No business logic.
|
||||
pkg/gotenberg/ → Core module system, interfaces, utilities, mocks.
|
||||
pkg/modules/ → Feature modules (api, chromium, libreoffice, pdfengines, etc.).
|
||||
pkg/standard/ → Wires all standard modules together via imports.
|
||||
test/integration/ → Gherkin feature files + Go test infrastructure.
|
||||
build/ → Dockerfile, fonts, Chromium config.
|
||||
.bruno/ → Bruno API collection (mirrors every route).
|
||||
```
|
||||
|
||||
Key interfaces live in `pkg/gotenberg/` — `Module`, `Provisioner`, `Validator`, `Debuggable`. Every module implements `Descriptor()` and self-registers. When adding features, determine if they belong in an existing module or require a new one.
|
||||
|
||||
## Codebase Navigation
|
||||
|
||||
- Start with `pkg/gotenberg/` for core interfaces and `pkg/modules/` for feature implementations.
|
||||
- The integration test infrastructure in `test/integration/scenario/` is well-structured — read `scenario.go` and `containers.go` to understand the Gherkin step definitions before writing new tests.
|
||||
- Mocks for all major interfaces are in `pkg/gotenberg/mocks.go` — use them for unit tests rather than creating new ones.
|
||||
- Import ordering is enforced: standard library, third-party, then `github.com/gotenberg/gotenberg/v8` — separated by blank lines.
|
||||
- When making changes, run only the relevant integration test tag rather than the full suite (40min timeout).
|
||||
|
||||
---
|
||||
|
||||
## Makefile — the Only Build Interface
|
||||
|
||||
All build and verification tasks go through the Makefile. Do not run `go` commands directly unless debugging a specific package.
|
||||
|
||||
| Command | Purpose | When to use |
|
||||
| ----------------------- | ------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `make build` | Build the Docker image | Before integration tests, or to verify compilation |
|
||||
| `make run` | Run a Gotenberg container locally | Manual testing. Flags are configured via `.env` and Makefile variables |
|
||||
| `make fmt` | Format Go code (`go fix`, `golangci-lint fmt`, `go mod tidy`) | Before every commit |
|
||||
| `make lint` | Lint Go code (strict `.golangci.yml` config) | Before every commit. Zero errors permitted |
|
||||
| `make lint-prettier` | Lint non-Go files (Markdown, YAML, etc.) with Prettier | Before every commit |
|
||||
| `make prettify` | Format non-Go files (Markdown, YAML, etc.) with Prettier | Before every commit |
|
||||
| `make test-unit` | Run unit tests (`go test -race ./...`) | After code changes to `pkg/` |
|
||||
| `make test-integration` | Run integration tests (Gherkin/Godog, 40min timeout) | After any feature or route change |
|
||||
| `make godoc` | Serve GoDoc at `localhost:6060` | To verify documentation |
|
||||
|
||||
## Module System
|
||||
|
||||
Gotenberg uses a self-registering module architecture inspired by CaddyServer. Each module:
|
||||
|
||||
- Lives in `pkg/modules/<name>/`
|
||||
- Implements the `gotenberg.Module` interface (at minimum `Descriptor()`)
|
||||
- May also implement `gotenberg.Provisioner`, `gotenberg.Validator`, or `gotenberg.Debuggable`
|
||||
- Self-registers via `init()` and is wired through `pkg/standard/`
|
||||
|
||||
When adding a feature, first determine if it belongs in an existing module. Only create a new module if the feature represents a genuinely separate concern.
|
||||
|
||||
## Commit Convention
|
||||
|
||||
Commits must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
```
|
||||
|
||||
Common types: `feat`, `fix`, `refactor`, `test`, `docs`, `chore`, `ci`, `build`. The scope should match the module or area of the change (e.g., `chromium`, `pdfengines`, `api`).
|
||||
|
||||
## Coding Patterns
|
||||
|
||||
- **Error handling:** Always wrap errors with context using `fmt.Errorf("description: %w", err)`. Never swallow errors silently.
|
||||
- **Import ordering:** Enforced by `gci` — standard library, then third-party, then `github.com/gotenberg/gotenberg/v8`. Three groups separated by blank lines.
|
||||
- **Mocks:** Comprehensive mock implementations for all major interfaces live in `pkg/gotenberg/mocks.go`. Use these for unit tests.
|
||||
- **No business logic in `cmd/`:** The `cmd/gotenberg/` package is strictly for wiring and startup.
|
||||
|
||||
## Adding PDF Engine Features
|
||||
|
||||
When adding a new PDF engine capability (e.g., bookmarks, watermark, stamp, embed), you must update the Makefile to include the corresponding engine list variable and flag. Every `--pdfengines-*-engines` flag registered in `pkg/modules/pdfengines/pdfengines.go` must have a matching entry in the Makefile:
|
||||
|
||||
1. **Add a variable** in the Makefile's variable block (around line 60-70):
|
||||
```makefile
|
||||
PDFENGINES_<FEATURE>_ENGINES=<default engines>
|
||||
```
|
||||
2. **Add the flag** in the Makefile's command args block (around line 140-155):
|
||||
```makefile
|
||||
--pdfengines-<feature>-engines=$(PDFENGINES_<FEATURE>_ENGINES) \
|
||||
```
|
||||
|
||||
The default value should match what is defined in `pdfengines.go`'s `fs.StringSlice(...)` call for that flag.
|
||||
|
||||
---
|
||||
|
||||
## Integration Tests
|
||||
|
||||
- **Framework:** Gherkin (BDD) via [Godog](https://github.com/cucumber/godog), with `testcontainers-go` for Docker orchestration.
|
||||
- **Feature files:** `test/integration/features/*.feature` — one file per endpoint or capability.
|
||||
- **Test infrastructure:** `test/integration/scenario/` — Go step definitions, container management, HTTP helpers, PDF validation.
|
||||
- **Entry point:** `test/integration/main_test.go` (build tag: `integration`).
|
||||
- **Test data:** `test/integration/testdata/`
|
||||
|
||||
### How It Works
|
||||
|
||||
Each scenario spins up a fresh Gotenberg Docker container via testcontainers. The step definitions in `scenario/scenario.go` map Gherkin steps to Go functions. An additional `gotenberg/integration-tools` container provides PDF validation tools (`verapdf`, `pdfinfo`, `pdftotext`).
|
||||
|
||||
**Important:** Integration tests require a Docker image. Run `make build` before `make test-integration`.
|
||||
|
||||
### Selective Test Runs
|
||||
|
||||
Use the `TAGS` variable to run only relevant scenarios:
|
||||
|
||||
```bash
|
||||
make test-integration TAGS=health
|
||||
make test-integration TAGS=chromium-convert-html
|
||||
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-watermark`, `watermark`, `pdfengines-stamp`, `stamp`, `pdfengines-bookmarks`, `bookmarks`, `pdfengines-rotate`, `rotate`, `prometheus-metrics`, `root`, `version`, `webhook`, `download-from`.
|
||||
|
||||
Other useful flags:
|
||||
|
||||
```bash
|
||||
make test-integration NO_CONCURRENCY=true # Disable parallel scenarios
|
||||
make test-integration PLATFORM=linux/arm64 # Force a specific platform
|
||||
```
|
||||
|
||||
### Writing a New Integration Test
|
||||
|
||||
1. Create or update a `.feature` file in `test/integration/features/`.
|
||||
2. Tag it appropriately (e.g., `@chromium @chromium-convert-html`).
|
||||
3. If the feature requires new tag(s), add them to both the `TAGS` comment block in the `Makefile` and the "Available tags" list in this file.
|
||||
4. If you create a new step definition, add it to `scenario/scenario.go`, register it in `InitializeScenario`, and update the "Available Gherkin Steps" list below.
|
||||
5. Test data goes in `test/integration/testdata/`.
|
||||
|
||||
### Available Gherkin Steps
|
||||
|
||||
**Given (setup):**
|
||||
|
||||
- `I have a default Gotenberg container`
|
||||
- `I have a Gotenberg container with the following environment variable(s):` (table: key | value)
|
||||
- `I have a (webhook|static) server`
|
||||
|
||||
**When (action):**
|
||||
|
||||
- `I make a "(GET|HEAD)" request to Gotenberg at the "<endpoint>" endpoint`
|
||||
- `I make a "(GET|HEAD)" request to Gotenberg at the "<endpoint>" endpoint with the following header(s):` (table: name | value)
|
||||
- `I make a "(POST)" request to Gotenberg at the "<endpoint>" endpoint with the following form data and header(s):` (table: name | value | kind — where kind is `file`, `field`, or `header`)
|
||||
- `I make <N> concurrent "(POST)" requests to Gotenberg at the "<endpoint>" endpoint with the following form data and header(s):` (same table format)
|
||||
- `I wait for the asynchronous request to the webhook`
|
||||
|
||||
**Then (assertions):**
|
||||
|
||||
- `the response status code should be <code>`
|
||||
- `the (response|webhook request) header "<name>" should be "<value>"`
|
||||
- `the (response|webhook request) cookie "<name>" should be "<value>"`
|
||||
- `the (response|webhook request) body should match string:` (docstring)
|
||||
- `the (response|webhook request) body should contain string:` (docstring)
|
||||
- `the (response|webhook request) body should match JSON:` (docstring — use `"ignore"` for dynamic values like timestamps)
|
||||
- `there should be <N> PDF(s) in the (response|webhook request)`
|
||||
- `there should be the following file(s) in the (response|webhook request):` (table of filenames)
|
||||
- `the "<name>" PDF should have <N> page(s)`
|
||||
- `the "<name>" PDF (should|should NOT) be set to landscape orientation`
|
||||
- `the "<name>" PDF (should|should NOT) have the following content at page <N>:` (docstring)
|
||||
- `the (response|webhook request) PDF(s) should be valid "<standard>" with a tolerance of <N> failed rule(s)` (standards: `PDF/A-1b`, `PDF/A-2b`, `PDF/A-3b`, `PDF/UA-1`, `PDF/UA-2`)
|
||||
- `the (response|webhook request) PDF(s) (should|should NOT) be flatten`
|
||||
- `the (response|webhook request) PDF(s) (should|should NOT) be encrypted`
|
||||
- `the (response|webhook request) PDF(s) (should|should NOT) have the "<filename>" file embedded`
|
||||
- `the Gotenberg container (should|should NOT) log the following entries:` (table of log substrings)
|
||||
- `all concurrent response status codes should be <code>`
|
||||
- `all concurrent responses should have <N> PDF(s)`
|
||||
|
||||
---
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
- [ ] No existing CLI flags renamed or removed
|
||||
- [ ] No existing environment variables renamed or removed
|
||||
- [ ] No existing API form fields renamed or removed
|
||||
- [ ] No existing HTTP endpoints changed or removed
|
||||
- [ ] No changes to default values that alter existing behavior
|
||||
|
||||
If any of these are violated, the change **must** be flagged as a breaking change.
|
||||
|
||||
### Linting Standards
|
||||
|
||||
The `.golangci.yml` enforces strict rules including: `gosec`, `govet`, `errcheck`, `staticcheck`, `dupl`, `bodyclose`, `exhaustive`, `errname`, and more. Zero linting errors are permitted.
|
||||
|
||||
Formatters enforce `gci`, `gofmt`, `gofumpt`, `goimports` with import ordering:
|
||||
|
||||
1. Standard library
|
||||
2. Third-party packages
|
||||
3. `github.com/gotenberg/gotenberg/v8`
|
||||
|
||||
Three groups separated by blank lines.
|
||||
|
||||
### Code Quality
|
||||
|
||||
- Errors are wrapped with context: `fmt.Errorf("description: %w", err)`. No swallowed errors.
|
||||
- No business logic in `cmd/`.
|
||||
- No panics in production code paths.
|
||||
- Input is validated defensively.
|
||||
- New features belong in the correct module (or justify a new one).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Every exported function, type, constant, and variable has a GoDoc comment starting with its name.
|
||||
- New packages include a `doc.go` file.
|
||||
- `README.md` is not modified unless explicitly requested.
|
||||
|
||||
### Definition of Done
|
||||
|
||||
A change is ready to merge only when:
|
||||
|
||||
1. Code compiles: `go build ./...`
|
||||
2. Code is formatted: `make fmt`
|
||||
3. All linters pass: `make lint` and `make lint-prettier`
|
||||
4. Integration tests pass: `make test-integration` (at minimum, the relevant `TAGS`)
|
||||
5. Unit tests pass: `make test-unit`
|
||||
6. All exported symbols and new packages have compliant GoDoc
|
||||
7. Bruno collection is updated (if routes were added or modified)
|
||||
|
||||
---
|
||||
|
||||
## Bruno API Collection
|
||||
|
||||
A [Bruno](https://www.usebruno.com/) collection lives in `.bruno/` and mirrors every Gotenberg route. When adding or updating a route, update the collection to match.
|
||||
|
||||
### Structure
|
||||
|
||||
```
|
||||
.bruno/
|
||||
├── bruno.json # Collection config
|
||||
├── collection.bru # Collection-level defaults (Gotenberg-Trace header)
|
||||
├── environments/
|
||||
│ ├── Local.bru # baseUrl: http://localhost:3000
|
||||
│ └── Demo.bru # baseUrl: https://demo.gotenberg.dev
|
||||
├── Health & Info/ # GET routes
|
||||
├── Chromium/Convert/ # POST routes grouped by module
|
||||
├── Chromium/Screenshot/
|
||||
├── LibreOffice/
|
||||
└── PDF Engines/<Feature>/ # One folder per feature (Merge, Split, Rotate, …)
|
||||
```
|
||||
|
||||
### `.bru` file format
|
||||
|
||||
```bru
|
||||
meta {
|
||||
name: <Human-readable name>
|
||||
type: http
|
||||
seq: <order within folder>
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/forms/<path>
|
||||
body: multipartForm
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
files: @file(../../test/integration/testdata/<file>)
|
||||
<mandatoryField>: <value>
|
||||
~<optionalField>: <value>
|
||||
}
|
||||
|
||||
headers {
|
||||
~Gotenberg-Output-Filename: <name>
|
||||
~Gotenberg-Webhook-Url: http://localhost:8080/webhook
|
||||
~Gotenberg-Webhook-Error-Url: http://localhost:8080/webhook/error
|
||||
~Gotenberg-Webhook-Method: POST
|
||||
~Gotenberg-Webhook-Error-Method: POST
|
||||
~Gotenberg-Webhook-Extra-Http-Headers: {"X-Custom":"value"}
|
||||
}
|
||||
```
|
||||
|
||||
### Conventions
|
||||
|
||||
- **Mandatory fields** are listed without prefix; **optional fields** are prefixed with `~` (disabled by default in Bruno).
|
||||
- **File references** use relative paths to `test/integration/testdata/`.
|
||||
- **Webhook and output filename headers** are included on every POST route as optional (`~`).
|
||||
- **One `.bru` file per request**. For routes with read/write variants (e.g., bookmarks, metadata), create separate files in the same folder.
|
||||
|
||||
### Checklist when adding/updating a route
|
||||
|
||||
1. Create or update the `.bru` file in the matching folder under `.bruno/`.
|
||||
2. Include all form fields from the route handler — check `FormData*` calls in the route function.
|
||||
3. For file upload fields (`files`, `watermark`, `stamp`, `embeds`), use `@file(...)` with a suitable test file.
|
||||
4. Verify the URL path matches the route's `Path` field exactly.
|
||||
5. If you add a new module folder, keep the naming consistent (e.g., `PDF Engines/Rotate/`).
|
||||
|
||||
3
CLAUDE.md
Normal file
3
CLAUDE.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Claude Code — Gotenberg
|
||||
|
||||
Read [AGENTS.md](AGENTS.md) first. It contains everything: core principles, project layout, coding standards, the mandatory 4-step workflow (Plan → Implement → Test → Review), integration test reference, review checklist, and Bruno collection guidelines.
|
||||
63
CONTRIBUTING.md
Normal file
63
CONTRIBUTING.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Contributing to Gotenberg
|
||||
|
||||
Thank you for your interest in contributing to Gotenberg! This guide will help you get started.
|
||||
|
||||
## Before You Start
|
||||
|
||||
Please read the [AGENTS.md](AGENTS.md) file — it describes the core principles, project layout, development standards, integration test reference, review checklist, and Bruno collection guidelines that all contributions must follow.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go (see version in `go.mod`)
|
||||
- Docker
|
||||
- Node.js (see version in `.node-version`) — for Prettier linting
|
||||
- [golangci-lint](https://golangci-lint.run/) v2+
|
||||
|
||||
### Build and Run
|
||||
|
||||
```bash
|
||||
make build # Build the Docker image
|
||||
make run # Run a local Gotenberg container
|
||||
```
|
||||
|
||||
### Development Loop
|
||||
|
||||
```bash
|
||||
# Write your code, then:
|
||||
make fmt # Format Go code
|
||||
make prettify # Format non-Go files (Markdown, YAML, etc.)
|
||||
make lint # Lint Go code (zero errors permitted)
|
||||
make lint-prettier # Lint non-Go files
|
||||
make test-unit # Run unit tests
|
||||
make build # Build the Docker image (required before integration tests)
|
||||
make test-integration # Run all integration tests
|
||||
```
|
||||
|
||||
To run only the integration tests relevant to your change:
|
||||
|
||||
```bash
|
||||
make test-integration TAGS=health
|
||||
make test-integration TAGS=chromium-convert-html
|
||||
make test-integration TAGS="merge,split"
|
||||
```
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
Before opening a PR, verify:
|
||||
|
||||
1. Code compiles: `make build`
|
||||
2. Code is formatted: `make fmt` and `make prettify`
|
||||
3. All linters pass: `make lint` and `make lint-prettier`
|
||||
4. Integration tests pass: `make test-integration` (at minimum, the relevant tags)
|
||||
5. Unit tests pass: `make test-unit`
|
||||
6. All exported symbols and new packages have GoDoc comments
|
||||
|
||||
### Guidelines
|
||||
|
||||
- **Conventional Commits.** Commit messages must follow the [Conventional Commits](https://www.conventionalcommits.org/) specification (e.g., `feat(chromium): add screenshot endpoint`, `fix(api): handle empty body`).
|
||||
- **One thing per PR.** Keep features, bug fixes, and refactoring in separate PRs.
|
||||
- **Backward compatibility matters.** Do not rename or remove existing CLI flags, environment variables, or API form fields without discussion.
|
||||
- **Integration tests first.** When adding a feature or route, start by writing the Gherkin scenario in `test/integration/features/`.
|
||||
- **No business logic in `cmd/`.** All logic belongs in `pkg/`.
|
||||
3
GEMINI.md
Normal file
3
GEMINI.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Gemini — Gotenberg
|
||||
|
||||
Read [AGENTS.md](AGENTS.md) first. It contains everything: core principles, project layout, coding standards, the mandatory 4-step workflow (Plan → Implement → Test → Review), integration test reference, review checklist, and Bruno collection guidelines.
|
||||
22
Makefile
22
Makefile
@@ -57,14 +57,19 @@ LOG_LEVEL=info
|
||||
LOG_FORMAT=auto
|
||||
LOG_FIELDS_PREFIX=
|
||||
LOG_ENABLE_GCP_FIELDS=false
|
||||
PDFENGINES_DISABLE_ROUTES=false
|
||||
PDFENGINES_MERGE_ENGINES=qpdf,pdfcpu,pdftk
|
||||
PDFENGINES_SPLIT_ENGINES=pdfcpu,qpdf,pdftk
|
||||
PDFENGINES_FLATTEN_ENGINES=qpdf
|
||||
PDFENGINES_CONVERT_ENGINES=libreoffice-pdfengine
|
||||
PDFENGINES_READ_METADATA_ENGINES=exiftool
|
||||
PDFENGINES_WRITE_METADATA_ENGINES=exiftool
|
||||
PDFENGINES_READ_BOOKMARKS_ENGINES=pdfcpu
|
||||
PDFENGINES_WRITE_BOOKMARKS_ENGINES=pdfcpu
|
||||
PDFENGINES_WATERMARK_ENGINES=pdfcpu,pdftk
|
||||
PDFENGINES_STAMP_ENGINES=pdfcpu,pdftk
|
||||
PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk
|
||||
PDFENGINES_DISABLE_ROUTES=false
|
||||
PDFENGINES_ROTATE_ENGINES=pdfcpu,pdftk
|
||||
PDFENGINES_EMBED_ENGINES=pdfcpu
|
||||
PROMETHEUS_NAMESPACE=gotenberg
|
||||
PROMETHEUS_COLLECT_INTERVAL=1s
|
||||
@@ -135,14 +140,19 @@ run: ## Start a Gotenberg container
|
||||
--log-format=$(LOG_FORMAT) \
|
||||
--log-fields-prefix=$(LOG_FIELDS_PREFIX) \
|
||||
--log-enable-gcp-fields=$(LOG_ENABLE_GCP_FIELDS) \
|
||||
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
||||
--pdfengines-merge-engines=$(PDFENGINES_MERGE_ENGINES) \
|
||||
--pdfengines-split-engines=$(PDFENGINES_SPLIT_ENGINES) \
|
||||
--pdfengines-flatten-engines=$(PDFENGINES_FLATTEN_ENGINES) \
|
||||
--pdfengines-convert-engines=$(PDFENGINES_CONVERT_ENGINES) \
|
||||
--pdfengines-read-metadata-engines=$(PDFENGINES_READ_METADATA_ENGINES) \
|
||||
--pdfengines-write-metadata-engines=$(PDFENGINES_WRITE_METADATA_ENGINES) \
|
||||
--pdfengines-read-bookmarks-engines=$(PDFENGINES_READ_BOOKMARKS_ENGINES) \
|
||||
--pdfengines-write-bookmarks-engines=$(PDFENGINES_WRITE_BOOKMARKS_ENGINES) \
|
||||
--pdfengines-watermark-engines=$(PDFENGINES_WATERMARK_ENGINES) \
|
||||
--pdfengines-stamp-engines=$(PDFENGINES_STAMP_ENGINES) \
|
||||
--pdfengines-encrypt-engines=$(PDFENGINES_ENCRYPT_ENGINES) \
|
||||
--pdfengines-disable-routes=$(PDFENGINES_DISABLE_ROUTES) \
|
||||
--pdfengines-rotate-engines=$(PDFENGINES_ROTATE_ENGINES) \
|
||||
--pdfengines-embed-engines=$(PDFENGINES_EMBED_ENGINES) \
|
||||
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
|
||||
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
|
||||
@@ -191,6 +201,14 @@ NO_CONCURRENCY=false
|
||||
# metadata
|
||||
# pdfengines-split
|
||||
# split
|
||||
# pdfengines-watermark
|
||||
# watermark
|
||||
# pdfengines-stamp
|
||||
# stamp
|
||||
# pdfengines-rotate
|
||||
# rotate
|
||||
# pdfengines-bookmarks
|
||||
# bookmarks
|
||||
# prometheus-metrics
|
||||
# root
|
||||
# version
|
||||
|
||||
37
go.mod
37
go.mod
@@ -19,15 +19,15 @@ require (
|
||||
github.com/mholt/archives v0.1.5
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/shirou/gopsutil/v4 v4.26.1
|
||||
github.com/shirou/gopsutil/v4 v4.26.2
|
||||
github.com/spf13/pflag v1.0.10
|
||||
github.com/testcontainers/testcontainers-go v0.40.0
|
||||
github.com/testcontainers/testcontainers-go v0.41.0
|
||||
go.uber.org/multierr v1.11.0
|
||||
go.uber.org/zap v1.27.1
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/text v0.34.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/term v0.41.0
|
||||
golang.org/x/text v0.35.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -42,6 +42,7 @@ require (
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -55,7 +56,7 @@ require (
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
@@ -78,7 +79,7 @@ require (
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mikelolasagasti/xz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.0.1 // indirect
|
||||
github.com/minio/minlz v1.1.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
@@ -91,13 +92,13 @@ require (
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.25 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.19.2 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.4 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
@@ -109,17 +110,17 @@ require (
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
86
go.sum
86
go.sum
@@ -26,6 +26,8 @@ github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
|
||||
@@ -70,8 +72,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
|
||||
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
|
||||
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -164,8 +166,8 @@ 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/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=
|
||||
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/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/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
|
||||
@@ -194,8 +196,8 @@ 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/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/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/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -208,13 +210,13 @@ 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/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
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/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1 h1:TOkEyriIXk2HX9d4isZJtbjXbEjf5qyKPAzbzY0JWSo=
|
||||
github.com/shirou/gopsutil/v4 v4.26.1/go.mod h1:medLI9/UNAb0dOI9Q3/7yWSqKkj00u+1tgY8nvv41pc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=
|
||||
github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||
github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=
|
||||
@@ -238,8 +240,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
|
||||
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
|
||||
github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais=
|
||||
github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
@@ -257,22 +259,22 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
||||
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/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/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||
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/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/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/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||
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/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/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
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/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -281,29 +283,29 @@ 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/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
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/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw=
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
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/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
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=
|
||||
google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
|
||||
@@ -46,14 +46,20 @@ func (mod *DebuggableMock) Debug() map[string]any {
|
||||
//
|
||||
//nolint:dupl
|
||||
type PdfEngineMock struct {
|
||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
SplitMock func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath 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
|
||||
MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error
|
||||
SplitMock func(ctx context.Context, logger *zap.Logger, mode SplitMode, inputPath, outputDirPath string) ([]string, error)
|
||||
FlattenMock func(ctx context.Context, logger *zap.Logger, inputPath string) error
|
||||
ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error
|
||||
ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||
PageCountMock func(ctx context.Context, logger *zap.Logger, inputPath string) (int, error)
|
||||
WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error
|
||||
ReadBookmarksMock func(ctx context.Context, logger *zap.Logger, inputPath string) ([]Bookmark, 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
|
||||
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
|
||||
RotateMock func(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
@@ -76,10 +82,18 @@ func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger *zap.Logge
|
||||
return engine.ReadMetadataMock(ctx, logger, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
return engine.PageCountMock(ctx, logger, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return engine.WriteMetadataMock(ctx, logger, metadata, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]Bookmark, error) {
|
||||
return engine.ReadBookmarksMock(ctx, logger, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return engine.EncryptMock(ctx, logger, inputPath, userPassword, ownerPassword)
|
||||
}
|
||||
@@ -88,6 +102,22 @@ func (engine *PdfEngineMock) EmbedFiles(ctx context.Context, logger *zap.Logger,
|
||||
return engine.EmbedFilesMock(ctx, logger, filePaths, inputPath)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []Bookmark) error {
|
||||
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)
|
||||
}
|
||||
|
||||
func (engine *PdfEngineMock) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
return engine.RotateMock(ctx, logger, inputPath, angle, pages)
|
||||
}
|
||||
|
||||
// PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface.
|
||||
type PdfEngineProviderMock struct {
|
||||
PdfEngineMock func() (PdfEngine, error)
|
||||
|
||||
@@ -28,6 +28,14 @@ var (
|
||||
// ErrPdfEncryptionNotSupported is returned when encryption
|
||||
// is not supported by the PDF engine.
|
||||
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")
|
||||
|
||||
// ErrPdfRotateAngleNotSupported is returned when the rotation angle is
|
||||
// not supported.
|
||||
ErrPdfRotateAngleNotSupported = errors.New("rotation angle not supported")
|
||||
)
|
||||
|
||||
// PdfEngineInvalidArgsError represents an error returned by a PDF engine when
|
||||
@@ -49,6 +57,33 @@ func NewPdfEngineInvalidArgs(engine, msg string) error {
|
||||
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 (
|
||||
// SplitModeIntervals represents a mode where a PDF is split at specific
|
||||
// intervals.
|
||||
@@ -109,6 +144,14 @@ type PdfFormats struct {
|
||||
PdfUa bool
|
||||
}
|
||||
|
||||
// Bookmark represents a node in the PDF document's outline
|
||||
// (table of contents).
|
||||
type Bookmark struct {
|
||||
Title string `json:"title"`
|
||||
Page int `json:"page"`
|
||||
Children []Bookmark `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
// PdfEngine provides an interface for operations on PDFs. Implementations
|
||||
// can use various tools like PDFtk, or implement functionality directly in
|
||||
// Go.
|
||||
@@ -135,9 +178,19 @@ type PdfEngine interface {
|
||||
// ReadMetadata extracts the metadata of a given PDF file.
|
||||
ReadMetadata(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error)
|
||||
|
||||
// PageCount returns the number of pages in a PDF file.
|
||||
PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error)
|
||||
|
||||
// WriteMetadata writes the metadata into a given PDF file.
|
||||
WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error
|
||||
|
||||
// ReadBookmarks reads the document outline (bookmarks) of a PDF file.
|
||||
ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]Bookmark, error)
|
||||
|
||||
// WriteBookmarks adds a document outline (bookmarks) to a PDF file.
|
||||
// The bookmarks parameter represents the hierarchical tree of the outline.
|
||||
WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []Bookmark) error
|
||||
|
||||
// Encrypt adds password protection to a PDF file.
|
||||
// The userPassword is required to open the document.
|
||||
// The ownerPassword provides full access to the document.
|
||||
@@ -146,7 +199,18 @@ type PdfEngine interface {
|
||||
|
||||
// EmbedFiles embeds files into a PDF. All files are embedded as file attachments
|
||||
// without modifying the main PDF content.
|
||||
// TODO: attachments instead? Rename the route?
|
||||
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
|
||||
|
||||
// Rotate rotates pages of a PDF file by the given angle (90, 180, 270).
|
||||
// If pages is empty, all pages are rotated.
|
||||
Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error
|
||||
}
|
||||
|
||||
// PdfEngineProvider offers an interface to instantiate a [PdfEngine].
|
||||
|
||||
@@ -81,8 +81,14 @@ type downloadFrom struct {
|
||||
// ExtraHttpHeaders are the HTTP headers to send alongside.
|
||||
ExtraHttpHeaders map[string]string `json:"extraHttpHeaders"`
|
||||
|
||||
// Download as embed file
|
||||
// Embedded routes the downloaded file as an embed. Deprecated: use
|
||||
// Field instead. Kept for backward compatibility.
|
||||
Embedded bool `json:"embedded"`
|
||||
|
||||
// Field routes the downloaded file to a specific form field bucket.
|
||||
// Supported values: "watermark", "stamp". For embeds, prefer the
|
||||
// Embedded flag or set Field to "embedded".
|
||||
Field string `json:"field"`
|
||||
}
|
||||
|
||||
// newContext returns a [Context] by parsing a "multipart/form-data" request.
|
||||
@@ -323,8 +329,15 @@ func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg.FileSyst
|
||||
}
|
||||
|
||||
ctx.files[filename] = path
|
||||
if dl.Embedded {
|
||||
|
||||
// Route the downloaded file to the appropriate field bucket.
|
||||
switch {
|
||||
case dl.Field == "embedded" || dl.Embedded:
|
||||
ctx.filesByField[EmbedsFormField] = append(ctx.filesByField[EmbedsFormField], path)
|
||||
case dl.Field == "watermark":
|
||||
ctx.filesByField[WatermarkFormField] = append(ctx.filesByField[WatermarkFormField], path)
|
||||
case dl.Field == "stamp":
|
||||
ctx.filesByField[StampFormField] = append(ctx.filesByField[StampFormField], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -17,9 +17,15 @@ import (
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
// EmbedsFormField represents the form field name for embedding files.
|
||||
const (
|
||||
// EmbedsFormField represents the form field name for embedding files.
|
||||
EmbedsFormField string = "embeds"
|
||||
|
||||
// WatermarkFormField represents the form field name for the watermark file.
|
||||
WatermarkFormField string = "watermark"
|
||||
|
||||
// StampFormField represents the form field name for the stamp file.
|
||||
StampFormField string = "stamp"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Watermark binds the absolute path of the form data file that should be
|
||||
// used as a watermark source. Only a file uploaded with the "watermark"
|
||||
// field name will be included.
|
||||
func (form *FormData) Watermark(target *string) *FormData {
|
||||
if form.errors != nil {
|
||||
return form
|
||||
}
|
||||
|
||||
if paths, ok := form.filesByField[WatermarkFormField]; ok && len(paths) > 0 {
|
||||
*target = paths[0]
|
||||
}
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
// Stamp binds the absolute path of the form data file that should be
|
||||
// used as a stamp source. Only a file uploaded with the "stamp"
|
||||
// field name will be included.
|
||||
func (form *FormData) Stamp(target *string) *FormData {
|
||||
if form.errors != nil {
|
||||
return form
|
||||
}
|
||||
|
||||
if paths, ok := form.filesByField[StampFormField]; ok && len(paths) > 0 {
|
||||
*target = paths[0]
|
||||
}
|
||||
|
||||
return form
|
||||
}
|
||||
|
||||
// paths bind the absolute paths of form data files, according to a list of
|
||||
// file extensions, to a string slice variable.
|
||||
// embeds are excluded.
|
||||
// embeds, watermark, and stamp files are excluded.
|
||||
func (form *FormData) paths(extensions []string, target *[]string) *FormData {
|
||||
embeds, ok := form.filesByField[EmbedsFormField]
|
||||
watermarks, wmOk := form.filesByField[WatermarkFormField]
|
||||
stamps, stOk := form.filesByField[StampFormField]
|
||||
|
||||
for filename, path := range form.files {
|
||||
if ok && slices.Contains(embeds, path) {
|
||||
continue
|
||||
}
|
||||
|
||||
if wmOk && slices.Contains(watermarks, path) {
|
||||
continue
|
||||
}
|
||||
|
||||
if stOk && slices.Contains(stamps, path) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ext := range extensions {
|
||||
// See https://github.com/gotenberg/gotenberg/issues/228.
|
||||
if strings.ToLower(filepath.Ext(filename)) == ext {
|
||||
|
||||
@@ -61,6 +61,14 @@ 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"
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
if errors.Is(err, gotenberg.ErrPdfRotateAngleNotSupported) {
|
||||
return http.StatusBadRequest, "At least one PDF engine cannot process the requested rotation angle, while others may have failed due to different issues"
|
||||
}
|
||||
|
||||
var invalidArgsError *gotenberg.PdfEngineInvalidArgsError
|
||||
if errors.As(err, &invalidArgsError) {
|
||||
return http.StatusBadRequest, invalidArgsError.Error()
|
||||
|
||||
@@ -415,6 +415,11 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||
stampFile := pdfengines.FormDataPdfStampFile(form)
|
||||
rotateAngle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||
|
||||
var url string
|
||||
err := form.
|
||||
@@ -424,7 +429,14 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
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) && watermarkFile != "" {
|
||||
watermark.Expression = watermarkFile
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && stampFile != "" {
|
||||
stamp.Expression = stampFile
|
||||
}
|
||||
|
||||
err = convertUrl(ctx, chromium, engine, url, options, mode, pdfFormats, metadata, userPassword, ownerPassword, embedPaths, watermark, stamp, rotateAngle, rotatePages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert URL to PDF: %w", err)
|
||||
}
|
||||
@@ -478,6 +490,11 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||
stampFile := pdfengines.FormDataPdfStampFile(form)
|
||||
rotateAngle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||
|
||||
var inputPath string
|
||||
err := form.
|
||||
@@ -487,8 +504,15 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && watermarkFile != "" {
|
||||
watermark.Expression = watermarkFile
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && stampFile != "" {
|
||||
stamp.Expression = stampFile
|
||||
}
|
||||
|
||||
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, rotateAngle, rotatePages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert HTML to PDF: %w", err)
|
||||
}
|
||||
@@ -543,6 +567,11 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||
stampFile := pdfengines.FormDataPdfStampFile(form)
|
||||
rotateAngle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||
|
||||
var (
|
||||
inputPath string
|
||||
@@ -557,12 +586,19 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && watermarkFile != "" {
|
||||
watermark.Expression = watermarkFile
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && stampFile != "" {
|
||||
stamp.Expression = stampFile
|
||||
}
|
||||
|
||||
url, err := markdownToHtml(ctx, inputPath, markdownPaths)
|
||||
if err != nil {
|
||||
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, rotateAngle, rotatePages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||
}
|
||||
@@ -686,7 +722,7 @@ func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string)
|
||||
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, rotateAngle int, rotatePages string) error {
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
// See https://github.com/gotenberg/gotenberg/issues/1130.
|
||||
filename := ctx.OutputFilename(outputPath)
|
||||
@@ -748,26 +784,48 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
||||
return fmt.Errorf("convert to PDF: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputPaths, err := pdfengines.SplitPdfStub(ctx, engine, mode, []string{outputPath})
|
||||
if err != nil {
|
||||
return fmt.Errorf("split PDF: %w", err)
|
||||
}
|
||||
|
||||
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.RotateStub(ctx, engine, rotateAngle, rotatePages, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate PDFs: %w", err)
|
||||
}
|
||||
|
||||
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF(s): %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
// Metadata, embeds are written after Convert, as LibreOffice
|
||||
// strips them during PDF/A conversion.
|
||||
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
|
||||
@@ -203,6 +203,47 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m
|
||||
return nil
|
||||
}
|
||||
|
||||
// PageCount returns the number of pages in a PDF file using ExifTool.
|
||||
func (engine *ExifTool) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read metadata with ExifTool: %w", err)
|
||||
}
|
||||
|
||||
pageCountValue, ok := metadata["PageCount"]
|
||||
if !ok {
|
||||
return 0, errors.New("PageCount not found in metadata")
|
||||
}
|
||||
|
||||
switch val := pageCountValue.(type) {
|
||||
case int:
|
||||
return val, nil
|
||||
case int64:
|
||||
return int(val), nil
|
||||
case float64:
|
||||
return int(val), nil
|
||||
case string:
|
||||
var res int
|
||||
_, err := fmt.Sscanf(val, "%d", &res)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse PageCount string '%s': %w", val, err)
|
||||
}
|
||||
return res, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unexpected PageCount type '%T'", pageCountValue)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteBookmarks is not available in this implementation.
|
||||
func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
return fmt.Errorf("write PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// ReadBookmarks is not available in this implementation.
|
||||
func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
return nil, fmt.Errorf("read PDF bookmarks with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Encrypt is not available in this implementation.
|
||||
func (engine *ExifTool) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return fmt.Errorf("encrypt PDF using ExifTool: %w", gotenberg.ErrPdfEncryptionNotSupported)
|
||||
@@ -213,6 +254,21 @@ func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *zap.Logger, file
|
||||
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)
|
||||
}
|
||||
|
||||
// Rotate is not available in this implementation.
|
||||
func (engine *ExifTool) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
return fmt.Errorf("rotate PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*ExifTool)(nil)
|
||||
|
||||
@@ -147,6 +147,29 @@ type Options struct {
|
||||
// Possible values are: 75, 150, 300, 600 and 1200.
|
||||
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,
|
||||
// PDF/A-3b and PDF/UA.
|
||||
PdfFormats gotenberg.PdfFormats
|
||||
@@ -178,6 +201,12 @@ func DefaultOptions() Options {
|
||||
Quality: 90,
|
||||
ReduceImageResolution: false,
|
||||
MaxImageResolution: 300,
|
||||
NativeWatermarkText: "",
|
||||
NativeWatermarkColor: 8388223,
|
||||
NativeWatermarkFontHeight: 0,
|
||||
NativeWatermarkRotateAngle: 0,
|
||||
NativeWatermarkFontName: "Helvetica",
|
||||
NativeTiledWatermarkText: "",
|
||||
PdfFormats: gotenberg.PdfFormats{
|
||||
PdfA: "",
|
||||
PdfUa: false,
|
||||
|
||||
@@ -302,14 +302,38 @@ 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("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 {
|
||||
case "":
|
||||
case gotenberg.PdfA1b:
|
||||
args = append(args, "--export", "SelectPdfVersion=1")
|
||||
args = append(args, "--export", "SelectPdfVersion=1", "--export", "EmbedStandardFonts=true")
|
||||
case gotenberg.PdfA2b:
|
||||
args = append(args, "--export", "SelectPdfVersion=2")
|
||||
args = append(args, "--export", "SelectPdfVersion=2", "--export", "EmbedStandardFonts=true")
|
||||
case gotenberg.PdfA3b:
|
||||
args = append(args, "--export", "SelectPdfVersion=3")
|
||||
args = append(args, "--export", "SelectPdfVersion=3", "--export", "EmbedStandardFonts=true")
|
||||
default:
|
||||
return ErrInvalidPdfFormats
|
||||
}
|
||||
@@ -320,6 +344,7 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logger, inputP
|
||||
"--export", "PDFUACompliance=true",
|
||||
"--export", "UseTaggedPDF=true",
|
||||
"--export", "EnableTextAccessForAccessibilityTools=true",
|
||||
"--export", "EmbedStandardFonts=true",
|
||||
)
|
||||
} else {
|
||||
args = append(
|
||||
|
||||
@@ -91,6 +91,21 @@ func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *z
|
||||
return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// PageCount is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
return 0, fmt.Errorf("page count with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteBookmarks is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
return fmt.Errorf("write PDF bookmarks with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// ReadBookmarks is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
return nil, fmt.Errorf("read PDF bookmarks with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Encrypt is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return fmt.Errorf("encrypt PDF using LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
@@ -101,6 +116,21 @@ func (engine *LibreOfficePdfEngine) EmbedFiles(ctx context.Context, logger *zap.
|
||||
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)
|
||||
}
|
||||
|
||||
// Rotate is not available in this implementation.
|
||||
func (engine *LibreOfficePdfEngine) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
return fmt.Errorf("rotate PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*LibreOfficePdfEngine)(nil)
|
||||
|
||||
@@ -32,6 +32,11 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := pdfengines.FormDataPdfEncrypt(form)
|
||||
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
||||
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
||||
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
||||
stamp := pdfengines.FormDataPdfStamp(form, false)
|
||||
stampFile := pdfengines.FormDataPdfStampFile(form)
|
||||
angle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
||||
|
||||
zeroValuedSplitMode := gotenberg.SplitMode{}
|
||||
|
||||
@@ -60,6 +65,12 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
quality int
|
||||
reduceImageResolution bool
|
||||
maxImageResolution int
|
||||
nativeWatermarkText string
|
||||
nativeWatermarkColor int
|
||||
nativeWatermarkFontHeight int
|
||||
nativeWatermarkRotateAngle int
|
||||
nativeWatermarkFontName string
|
||||
nativeTiledWatermarkText string
|
||||
nativePdfFormats bool
|
||||
merge bool
|
||||
flatten bool
|
||||
@@ -128,6 +139,48 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
maxImageResolution = intValue
|
||||
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("merge", &merge, false).
|
||||
Bool("flatten", &flatten, false).
|
||||
@@ -136,6 +189,21 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && watermarkFile != "" {
|
||||
watermark.Expression = watermarkFile
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && stampFile != "" {
|
||||
stamp.Expression = stampFile
|
||||
}
|
||||
|
||||
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hasPostProcessing := watermark.Source != "" || stamp.Source != "" || angle != 0 ||
|
||||
len(embedPaths) > 0 || len(metadata) > 0 || flatten
|
||||
|
||||
outputPaths := make([]string, len(inputPaths))
|
||||
for i, inputPath := range inputPaths {
|
||||
outputPaths[i] = ctx.GeneratePath(".pdf")
|
||||
@@ -163,11 +231,18 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
Quality: quality,
|
||||
ReduceImageResolution: reduceImageResolution,
|
||||
MaxImageResolution: maxImageResolution,
|
||||
NativeWatermarkText: nativeWatermarkText,
|
||||
NativeWatermarkColor: nativeWatermarkColor,
|
||||
NativeWatermarkFontHeight: nativeWatermarkFontHeight,
|
||||
NativeWatermarkRotateAngle: nativeWatermarkRotateAngle,
|
||||
NativeWatermarkFontName: nativeWatermarkFontName,
|
||||
NativeTiledWatermarkText: nativeTiledWatermarkText,
|
||||
}
|
||||
|
||||
if nativePdfFormats && splitMode == zeroValuedSplitMode {
|
||||
if nativePdfFormats && splitMode == zeroValuedSplitMode && !hasPostProcessing {
|
||||
// Only natively apply given PDF formats if we're not
|
||||
// splitting the PDF later.
|
||||
// splitting the PDF later and no post-processing features
|
||||
// are enabled (as they would degrade compliance).
|
||||
options.PdfFormats = pdfFormats
|
||||
}
|
||||
|
||||
@@ -233,7 +308,32 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}
|
||||
}
|
||||
|
||||
if !nativePdfFormats || (nativePdfFormats && splitMode != zeroValuedSplitMode) {
|
||||
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.RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate PDFs: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
needsConvertStub := !nativePdfFormats ||
|
||||
(nativePdfFormats && splitMode != zeroValuedSplitMode) ||
|
||||
(nativePdfFormats && hasPostProcessing)
|
||||
if needsConvertStub {
|
||||
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
@@ -253,21 +353,16 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap
|
||||
}
|
||||
}
|
||||
|
||||
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
// Metadata, embeds are written after Convert, as LibreOffice
|
||||
// strips them during PDF/A conversion.
|
||||
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = pdfengines.EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package pdfcpu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
@@ -26,6 +29,16 @@ type PdfCpu struct {
|
||||
binPath string
|
||||
}
|
||||
|
||||
type pdfcpuBookmark struct {
|
||||
Title string `json:"title"`
|
||||
Page int `json:"page"`
|
||||
Children []pdfcpuBookmark `json:"kids,omitempty"`
|
||||
}
|
||||
|
||||
type pdfcpuBookmarks struct {
|
||||
Bookmarks []pdfcpuBookmark `json:"bookmarks"`
|
||||
}
|
||||
|
||||
// Descriptor returns a [PdfCpu]'s module descriptor.
|
||||
func (engine *PdfCpu) Descriptor() gotenberg.ModuleDescriptor {
|
||||
return gotenberg.ModuleDescriptor{
|
||||
@@ -171,6 +184,145 @@ func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, met
|
||||
return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// PageCount is not available in this implementation.
|
||||
func (engine *PdfCpu) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
return 0, fmt.Errorf("page count with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// ReadBookmarks reads the document outline (bookmarks) of a PDF file using pdfcpu.
|
||||
func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
tmpPath := fmt.Sprintf("%s.read.json", inputPath)
|
||||
args := []string{"bookmarks", "export", inputPath, tmpPath}
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.Remove(tmpPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
logger.Error(fmt.Sprintf("remove temporary bookmarks JSON file: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
_, cmdErr := cmd.Exec()
|
||||
|
||||
// Check file existence and size.
|
||||
info, statErr := os.Stat(tmpPath)
|
||||
|
||||
if cmdErr != nil {
|
||||
// If the file wasn't created, or it was created but is 0 bytes,
|
||||
// it means pdfcpu had no bookmarks to write.
|
||||
if os.IsNotExist(statErr) || (statErr == nil && info.Size() == 0) {
|
||||
return make([]gotenberg.Bookmark, 0), nil
|
||||
}
|
||||
|
||||
// Fallback: Check the error string just in case pdfcpu failed without
|
||||
// touching the file.
|
||||
if strings.Contains(strings.ToLower(cmdErr.Error()), "no bookmarks") {
|
||||
return make([]gotenberg.Bookmark, 0), nil
|
||||
}
|
||||
return nil, fmt.Errorf("read bookmarks with pdfcpu: %w", cmdErr)
|
||||
}
|
||||
|
||||
// If cmd succeeded, but output a 0-byte file anyway.
|
||||
if info != nil && info.Size() == 0 {
|
||||
return make([]gotenberg.Bookmark, 0), nil
|
||||
}
|
||||
|
||||
// Read the file content.
|
||||
jsonBytes, err := os.ReadFile(tmpPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read temporary bookmarks JSON file: %w", err)
|
||||
}
|
||||
|
||||
// Check if the content is just empty whitespace.
|
||||
if len(bytes.TrimSpace(jsonBytes)) == 0 {
|
||||
return make([]gotenberg.Bookmark, 0), nil
|
||||
}
|
||||
|
||||
var data pdfcpuBookmarks
|
||||
err = json.Unmarshal(jsonBytes, &data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal bookmarks: %w", err)
|
||||
}
|
||||
|
||||
// Safety check: Does the parsed JSON actually contain bookmarks?
|
||||
if len(data.Bookmarks) == 0 {
|
||||
return make([]gotenberg.Bookmark, 0), nil
|
||||
}
|
||||
|
||||
var mapBookmarks func(bookmarks []pdfcpuBookmark) []gotenberg.Bookmark
|
||||
mapBookmarks = func(bookmarks []pdfcpuBookmark) []gotenberg.Bookmark {
|
||||
res := make([]gotenberg.Bookmark, len(bookmarks))
|
||||
for i, b := range bookmarks {
|
||||
res[i] = gotenberg.Bookmark{
|
||||
Title: b.Title,
|
||||
Page: b.Page,
|
||||
Children: mapBookmarks(b.Children),
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return mapBookmarks(data.Bookmarks), nil
|
||||
}
|
||||
|
||||
// WriteBookmarks adds a document outline (bookmarks) to a PDF file using pdfcpu.
|
||||
func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
if len(bookmarks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var mapBookmarks func(bookmarks []gotenberg.Bookmark) []pdfcpuBookmark
|
||||
mapBookmarks = func(bookmarks []gotenberg.Bookmark) []pdfcpuBookmark {
|
||||
res := make([]pdfcpuBookmark, len(bookmarks))
|
||||
for i, b := range bookmarks {
|
||||
res[i] = pdfcpuBookmark{
|
||||
Title: b.Title,
|
||||
Page: b.Page,
|
||||
Children: mapBookmarks(b.Children),
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
data := pdfcpuBookmarks{
|
||||
Bookmarks: mapBookmarks(bookmarks),
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal bookmarks: %w", err)
|
||||
}
|
||||
|
||||
tmpPath := fmt.Sprintf("%s.json", inputPath)
|
||||
err = os.WriteFile(tmpPath, jsonBytes, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write temporary bookmarks JSON file: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err := os.Remove(tmpPath)
|
||||
if err != nil {
|
||||
logger.Error(fmt.Sprintf("remove temporary bookmarks JSON file: %v", err))
|
||||
}
|
||||
}()
|
||||
|
||||
args := []string{"bookmarks", "import", "-replace", inputPath, tmpPath, 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("write bookmarks with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmbedFiles embeds files into a PDF. All files are embedded as file attachments
|
||||
// without modifying the main PDF content.
|
||||
func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *zap.Logger, filePaths []string, inputPath string) error {
|
||||
@@ -228,6 +380,78 @@ func (engine *PdfCpu) Encrypt(ctx context.Context, logger *zap.Logger, inputPath
|
||||
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)
|
||||
}
|
||||
|
||||
// Rotate rotates pages of a PDF file by the given angle using pdfcpu.
|
||||
func (engine *PdfCpu) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
args := []string{"rotate"}
|
||||
if pages != "" {
|
||||
args = append(args, "-pages", pages)
|
||||
}
|
||||
args = append(args, "--", inputPath, strconv.Itoa(angle), inputPath)
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate PDF with pdfcpu: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (engine *PdfCpu) applyStampOrWatermark(ctx context.Context, logger *zap.Logger, command string, inputPath string, stamp gotenberg.Stamp) error {
|
||||
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.
|
||||
var (
|
||||
_ gotenberg.Module = (*PdfCpu)(nil)
|
||||
|
||||
@@ -12,14 +12,19 @@ import (
|
||||
)
|
||||
|
||||
type multiPdfEngines struct {
|
||||
mergeEngines []gotenberg.PdfEngine
|
||||
splitEngines []gotenberg.PdfEngine
|
||||
flattenEngines []gotenberg.PdfEngine
|
||||
convertEngines []gotenberg.PdfEngine
|
||||
readMetadataEngines []gotenberg.PdfEngine
|
||||
writeMetadataEngines []gotenberg.PdfEngine
|
||||
passwordEngines []gotenberg.PdfEngine
|
||||
embedEngines []gotenberg.PdfEngine
|
||||
mergeEngines []gotenberg.PdfEngine
|
||||
splitEngines []gotenberg.PdfEngine
|
||||
flattenEngines []gotenberg.PdfEngine
|
||||
convertEngines []gotenberg.PdfEngine
|
||||
readMetadataEngines []gotenberg.PdfEngine
|
||||
writeMetadataEngines []gotenberg.PdfEngine
|
||||
passwordEngines []gotenberg.PdfEngine
|
||||
embedEngines []gotenberg.PdfEngine
|
||||
readBookmarksEngines []gotenberg.PdfEngine
|
||||
writeBookmarksEngines []gotenberg.PdfEngine
|
||||
watermarkEngines []gotenberg.PdfEngine
|
||||
stampEngines []gotenberg.PdfEngine
|
||||
rotateEngines []gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
func newMultiPdfEngines(
|
||||
@@ -30,17 +35,27 @@ func newMultiPdfEngines(
|
||||
readMetadataEngines,
|
||||
writeMetadataEngines,
|
||||
passwordEngines,
|
||||
embedEngines []gotenberg.PdfEngine,
|
||||
embedEngines,
|
||||
readBookmarksEngines,
|
||||
writeBookmarksEngines,
|
||||
watermarkEngines,
|
||||
stampEngines,
|
||||
rotateEngines []gotenberg.PdfEngine,
|
||||
) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
mergeEngines: mergeEngines,
|
||||
splitEngines: splitEngines,
|
||||
flattenEngines: flattenEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMetadataEngines: readMetadataEngines,
|
||||
writeMetadataEngines: writeMetadataEngines,
|
||||
passwordEngines: passwordEngines,
|
||||
embedEngines: embedEngines,
|
||||
mergeEngines: mergeEngines,
|
||||
splitEngines: splitEngines,
|
||||
flattenEngines: flattenEngines,
|
||||
convertEngines: convertEngines,
|
||||
readMetadataEngines: readMetadataEngines,
|
||||
writeMetadataEngines: writeMetadataEngines,
|
||||
passwordEngines: passwordEngines,
|
||||
embedEngines: embedEngines,
|
||||
readBookmarksEngines: readBookmarksEngines,
|
||||
writeBookmarksEngines: writeBookmarksEngines,
|
||||
watermarkEngines: watermarkEngines,
|
||||
stampEngines: stampEngines,
|
||||
rotateEngines: rotateEngines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +231,103 @@ func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Log
|
||||
return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
type pageCountResult struct {
|
||||
pageCount int
|
||||
err error
|
||||
}
|
||||
|
||||
// PageCount returns the number of pages in a PDF file using the first available
|
||||
// engine that supports metadata reading.
|
||||
func (multi *multiPdfEngines) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
for _, engine := range multi.readMetadataEngines {
|
||||
resultChan := make(chan pageCountResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
pageCount, err := engine.PageCount(ctx, logger, inputPath)
|
||||
resultChan <- pageCountResult{pageCount: pageCount, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = multierr.Append(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
return result.pageCount, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("page count with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
type readBookmarksResult struct {
|
||||
bookmarks []gotenberg.Bookmark
|
||||
err error
|
||||
}
|
||||
|
||||
// ReadBookmarks reads bookmarks from a PDF file using the first available
|
||||
// engine that supports bookmarks reading.
|
||||
func (multi *multiPdfEngines) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
var err error
|
||||
var mu sync.Mutex // to safely append errors.
|
||||
|
||||
for _, engine := range multi.readBookmarksEngines {
|
||||
resultChan := make(chan readBookmarksResult, 1)
|
||||
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
bookmarks, err := engine.ReadBookmarks(ctx, logger, inputPath)
|
||||
resultChan <- readBookmarksResult{bookmarks: bookmarks, err: err}
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.err != nil {
|
||||
mu.Lock()
|
||||
err = multierr.Append(err, result.err)
|
||||
mu.Unlock()
|
||||
} else {
|
||||
return result.bookmarks, nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("read PDF bookmarks with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// WriteBookmarks adds a document outline (bookmarks) to a PDF file using the
|
||||
// first available engine that supports bookmarks writing.
|
||||
func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.writeBookmarksEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.WriteBookmarks(ctx, logger, inputPath, bookmarks)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case writeBookmarksErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, writeBookmarksErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("write PDF bookmarks with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using the first available
|
||||
// engine that supports password protection.
|
||||
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
@@ -266,6 +378,81 @@ func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *zap.Logger
|
||||
return fmt.Errorf("embed files into PDF using multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Watermark applies a watermark (behind page content) to a PDF file using the
|
||||
// first available engine that supports watermarking.
|
||||
func (multi *multiPdfEngines) Watermark(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.watermarkEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Watermark(ctx, logger, inputPath, stamp)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case watermarkErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, watermarkErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("watermark PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Stamp applies a stamp (on top of page content) to a PDF file using the
|
||||
// first available engine that supports stamping.
|
||||
func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *zap.Logger, inputPath string, stamp gotenberg.Stamp) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.stampEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Stamp(ctx, logger, inputPath, stamp)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case stampErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, stampErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Rotate rotates pages of a PDF file using the first available engine that
|
||||
// supports rotation.
|
||||
func (multi *multiPdfEngines) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
var err error
|
||||
errChan := make(chan error, 1)
|
||||
|
||||
for _, engine := range multi.rotateEngines {
|
||||
go func(engine gotenberg.PdfEngine) {
|
||||
errChan <- engine.Rotate(ctx, logger, inputPath, angle, pages)
|
||||
}(engine)
|
||||
|
||||
select {
|
||||
case rotateErr := <-errChan:
|
||||
errored := multierr.AppendInto(&err, rotateErr)
|
||||
if !errored {
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("rotate PDF with multi PDF engines: %w", err)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
package pdfengines
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestMultiPdfEngines_Merge(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
mergeEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
MergeMock: func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Merge(tc.ctx, zap.NewNop(), nil, "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Encrypt(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
passwordEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
passwordEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
passwordEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
passwordEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
EncryptMock: func(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Encrypt(tc.ctx, zap.NewNop(), "", "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Split(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
splitEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
SplitMock: func(ctx context.Context, logger *zap.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
_, err := tc.engine.Split(tc.ctx, zap.NewNop(), gotenberg.SplitMode{}, "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Flatten(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
flattenEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
FlattenMock: func(ctx context.Context, logger *zap.Logger, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Flatten(tc.ctx, zap.NewNop(), "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_Convert(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
convertEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ConvertMock: func(ctx context.Context, logger *zap.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.Convert(tc.ctx, zap.NewNop(), gotenberg.PdfFormats{}, "", "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_ReadMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return make(map[string]any), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return make(map[string]any), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return nil, errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
readMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
ReadMetadataMock: func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]any, error) {
|
||||
return make(map[string]any), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
_, err := tc.engine.ReadMetadata(tc.ctx, zap.NewNop(), "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiPdfEngines_WriteMetadata(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
scenario string
|
||||
engine *multiPdfEngines
|
||||
ctx context.Context
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
scenario: "nominal behavior",
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "at least one engine does not return an error",
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
},
|
||||
{
|
||||
scenario: "all engines return an error",
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return errors.New("foo")
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: context.Background(),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
scenario: "context expired",
|
||||
engine: &multiPdfEngines{
|
||||
writeMetadataEngines: []gotenberg.PdfEngine{
|
||||
&gotenberg.PdfEngineMock{
|
||||
WriteMetadataMock: func(ctx context.Context, logger *zap.Logger, metadata map[string]any, inputPath string) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ctx: func() context.Context {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
return ctx
|
||||
}(),
|
||||
expectError: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.scenario, func(t *testing.T) {
|
||||
err := tc.engine.WriteMetadata(tc.ctx, zap.NewNop(), nil, "")
|
||||
|
||||
if !tc.expectError && err != nil {
|
||||
t.Fatalf("expected no error but got: %v", err)
|
||||
}
|
||||
|
||||
if tc.expectError && err == nil {
|
||||
t.Fatal("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -28,16 +28,21 @@ func init() {
|
||||
// the [api.Router] interface to expose relevant PDF processing routes if
|
||||
// enabled.
|
||||
type PdfEngines struct {
|
||||
mergeNames []string
|
||||
splitNames []string
|
||||
flattenNames []string
|
||||
convertNames []string
|
||||
readMetadataNames []string
|
||||
writeMetadataNames []string
|
||||
encryptNames []string
|
||||
embedNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
mergeNames []string
|
||||
splitNames []string
|
||||
flattenNames []string
|
||||
convertNames []string
|
||||
readMetadataNames []string
|
||||
writeMetadataNames []string
|
||||
encryptNames []string
|
||||
embedNames []string
|
||||
readBookmarksNames []string
|
||||
writeBookmarksNames []string
|
||||
watermarkNames []string
|
||||
stampNames []string
|
||||
rotateNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
|
||||
// Descriptor returns a PdfEngines' module descriptor.
|
||||
@@ -54,6 +59,11 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
||||
fs.StringSlice("pdfengines-encrypt-engines", []string{"qpdf", "pdftk", "pdfcpu"}, "Set the PDF engines and their order for the password protection 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-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.StringSlice("pdfengines-rotate-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the rotate feature - empty means all")
|
||||
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
||||
|
||||
// Deprecated flags.
|
||||
@@ -81,6 +91,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
||||
encryptNames := flags.MustStringSlice("pdfengines-encrypt-engines")
|
||||
embedNames := flags.MustStringSlice("pdfengines-embed-engines")
|
||||
readBookmarksNames := flags.MustStringSlice("pdfengines-read-bookmarks-engines")
|
||||
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-bookmarks-engines")
|
||||
watermarkNames := flags.MustStringSlice("pdfengines-watermark-engines")
|
||||
stampNames := flags.MustStringSlice("pdfengines-stamp-engines")
|
||||
rotateNames := flags.MustStringSlice("pdfengines-rotate-engines")
|
||||
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
||||
|
||||
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
||||
@@ -147,6 +162,31 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.embedNames = embedNames
|
||||
}
|
||||
|
||||
mod.readBookmarksNames = defaultNames
|
||||
if len(readBookmarksNames) > 0 {
|
||||
mod.readBookmarksNames = readBookmarksNames
|
||||
}
|
||||
|
||||
mod.writeBookmarksNames = defaultNames
|
||||
if len(writeBookmarksNames) > 0 {
|
||||
mod.writeBookmarksNames = writeBookmarksNames
|
||||
}
|
||||
|
||||
mod.watermarkNames = defaultNames
|
||||
if len(watermarkNames) > 0 {
|
||||
mod.watermarkNames = watermarkNames
|
||||
}
|
||||
|
||||
mod.stampNames = defaultNames
|
||||
if len(stampNames) > 0 {
|
||||
mod.stampNames = stampNames
|
||||
}
|
||||
|
||||
mod.rotateNames = defaultNames
|
||||
if len(rotateNames) > 0 {
|
||||
mod.rotateNames = rotateNames
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -196,6 +236,11 @@ func (mod *PdfEngines) Validate() error {
|
||||
findNonExistingEngines(mod.writeMetadataNames)
|
||||
findNonExistingEngines(mod.encryptNames)
|
||||
findNonExistingEngines(mod.embedNames)
|
||||
findNonExistingEngines(mod.readBookmarksNames)
|
||||
findNonExistingEngines(mod.writeBookmarksNames)
|
||||
findNonExistingEngines(mod.watermarkNames)
|
||||
findNonExistingEngines(mod.stampNames)
|
||||
findNonExistingEngines(mod.rotateNames)
|
||||
|
||||
if len(nonExistingEngines) == 0 {
|
||||
return nil
|
||||
@@ -215,6 +260,12 @@ func (mod *PdfEngines) SystemMessages() []string {
|
||||
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")),
|
||||
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames[:], " ")),
|
||||
fmt.Sprintf("encrypt engines - %s", strings.Join(mod.encryptNames[:], " ")),
|
||||
fmt.Sprintf("embed engines - %s", strings.Join(mod.embedNames[:], " ")),
|
||||
fmt.Sprintf("read bookmarks engines - %s", strings.Join(mod.readBookmarksNames[:], " ")),
|
||||
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames[:], " ")),
|
||||
fmt.Sprintf("watermark engines - %s", strings.Join(mod.watermarkNames[:], " ")),
|
||||
fmt.Sprintf("stamp engines - %s", strings.Join(mod.stampNames[:], " ")),
|
||||
fmt.Sprintf("rotate engines - %s", strings.Join(mod.rotateNames[:], " ")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +294,11 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
engines(mod.writeMetadataNames),
|
||||
engines(mod.encryptNames),
|
||||
engines(mod.embedNames),
|
||||
engines(mod.readBookmarksNames),
|
||||
engines(mod.writeBookmarksNames),
|
||||
engines(mod.watermarkNames),
|
||||
engines(mod.stampNames),
|
||||
engines(mod.rotateNames),
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -266,8 +322,13 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
convertRoute(engine),
|
||||
readMetadataRoute(engine),
|
||||
writeMetadataRoute(engine),
|
||||
readBookmarksRoute(engine),
|
||||
writeBookmarksRoute(engine),
|
||||
encryptRoute(engine),
|
||||
embedRoute(engine),
|
||||
watermarkRoute(engine),
|
||||
stampRoute(engine),
|
||||
rotateRoute(engine),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg.SplitMod
|
||||
}
|
||||
|
||||
// FormDataPdfFormats creates [gotenberg.PdfFormats] from the form data.
|
||||
// Fallback to default value if the considered key is not present.
|
||||
// Fallback to the default value if the considered key is not present.
|
||||
func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||
var (
|
||||
pdfa string
|
||||
@@ -128,6 +128,129 @@ func FormDataPdfMetadata(form *api.FormData, mandatory bool) map[string]any {
|
||||
return metadata
|
||||
}
|
||||
|
||||
// FormDataPdfBookmarks creates bookmarks from the form data.
|
||||
func FormDataPdfBookmarks(form *api.FormData, mandatory bool) any {
|
||||
var bookmarks any
|
||||
|
||||
bookmarksFunc := func(value string) error {
|
||||
if len(value) > 0 {
|
||||
var list []gotenberg.Bookmark
|
||||
err := json.Unmarshal([]byte(value), &list)
|
||||
if err == nil {
|
||||
bookmarks = list
|
||||
return nil
|
||||
}
|
||||
|
||||
var m map[string][]gotenberg.Bookmark
|
||||
err = json.Unmarshal([]byte(value), &m)
|
||||
if err == nil {
|
||||
bookmarks = m
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("unmarshal bookmarks: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.MandatoryCustom("bookmarks", func(value string) error {
|
||||
return bookmarksFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.Custom("bookmarks", func(value string) error {
|
||||
return bookmarksFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
return bookmarks
|
||||
}
|
||||
|
||||
// FormDataPdfRotate creates rotation parameters from the form data.
|
||||
func FormDataPdfRotate(form *api.FormData, mandatory bool) (int, string) {
|
||||
var angle int
|
||||
var pages string
|
||||
|
||||
angleFunc := func(value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v != 90 && v != 180 && v != 270 {
|
||||
return errors.New("wrong value, expected 90, 180, or 270")
|
||||
}
|
||||
angle = v
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.MandatoryCustom("rotateAngle", func(value string) error {
|
||||
return angleFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.Custom("rotateAngle", func(value string) error {
|
||||
return angleFunc(value)
|
||||
})
|
||||
}
|
||||
form.String("rotatePages", &pages, "")
|
||||
|
||||
return angle, pages
|
||||
}
|
||||
|
||||
// RotateStub rotates pages of PDF files. If angle is 0, it does nothing.
|
||||
func RotateStub(ctx *api.Context, engine gotenberg.PdfEngine, angle int, pages string, inputPaths []string) error {
|
||||
if angle == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Rotate(ctx, ctx.Log(), inputPath, angle, pages)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidatePdfFormatsCompat checks for incompatible combinations of PDF formats
|
||||
// with other features and returns an appropriate error if found.
|
||||
func ValidatePdfFormatsCompat(pdfFormats gotenberg.PdfFormats, userPassword string, embedPaths []string) error {
|
||||
zeroValued := gotenberg.PdfFormats{}
|
||||
if pdfFormats == zeroValued {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PDF/A forbids encryption per the standard.
|
||||
if pdfFormats.PdfA != "" && userPassword != "" {
|
||||
return api.WrapError(
|
||||
errors.New("PDF/A format is incompatible with encryption"),
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusBadRequest,
|
||||
"Invalid form data: PDF/A format is incompatible with encryption",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// Only PDF/A-3 variants allow embedded file attachments.
|
||||
if pdfFormats.PdfA != "" && len(embedPaths) > 0 {
|
||||
if pdfFormats.PdfA != gotenberg.PdfA3a && pdfFormats.PdfA != gotenberg.PdfA3b && pdfFormats.PdfA != gotenberg.PdfA3u {
|
||||
return api.WrapError(
|
||||
fmt.Errorf("PDF format '%s' does not support embedded files", pdfFormats.PdfA),
|
||||
api.NewSentinelHttpError(
|
||||
http.StatusBadRequest,
|
||||
fmt.Sprintf("Invalid form data: PDF format '%s' does not support embedded files; only PDF/A-3 variants allow attachments", pdfFormats.PdfA),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MergeStub merges given PDFs. If only one input PDF, it does nothing and
|
||||
// returns the corresponding input path.
|
||||
func MergeStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths []string) (string, error) {
|
||||
@@ -254,6 +377,58 @@ func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata ma
|
||||
return nil
|
||||
}
|
||||
|
||||
func shiftBookmarks(bookmarks []gotenberg.Bookmark, offset int) []gotenberg.Bookmark {
|
||||
if offset == 0 {
|
||||
return bookmarks
|
||||
}
|
||||
shifted := make([]gotenberg.Bookmark, len(bookmarks))
|
||||
for i, b := range bookmarks {
|
||||
shifted[i] = gotenberg.Bookmark{
|
||||
Title: b.Title,
|
||||
Page: b.Page + offset,
|
||||
Children: shiftBookmarks(b.Children, offset),
|
||||
}
|
||||
}
|
||||
return shifted
|
||||
}
|
||||
|
||||
// WriteBookmarksStub writes the bookmarks into PDF files. If no bookmarks, it
|
||||
// does nothing.
|
||||
func WriteBookmarksStub(ctx *api.Context, engine gotenberg.PdfEngine, bookmarks any, inputPaths []string) error {
|
||||
if bookmarks == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch b := bookmarks.(type) {
|
||||
case []gotenberg.Bookmark:
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.WriteBookmarks(ctx, ctx.Log(), inputPath, b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bookmarks into '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
case map[string][]gotenberg.Bookmark:
|
||||
for _, inputPath := range inputPaths {
|
||||
filename := filepath.Base(inputPath)
|
||||
if specificBookmarks, ok := b[filename]; ok {
|
||||
err := engine.WriteBookmarks(ctx, ctx.Log(), inputPath, specificBookmarks)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bookmarks into '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Should not happen.
|
||||
return fmt.Errorf("bookmarks type '%T' not supported", bookmarks)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormDataPdfEmbeds extracts embedded file paths from form data.
|
||||
// Only files uploaded with the "embeds" field name are included.
|
||||
func FormDataPdfEmbeds(form *api.FormData) []string {
|
||||
@@ -301,6 +476,122 @@ func EmbedFilesStub(ctx *api.Context, engine gotenberg.PdfEngine, embedPaths []s
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormDataPdfWatermark creates a [gotenberg.Stamp] for watermarking from the
|
||||
// form data.
|
||||
func FormDataPdfWatermark(form *api.FormData, mandatory bool) gotenberg.Stamp {
|
||||
return formDataPdfStampOrWatermark(form, "watermark", mandatory)
|
||||
}
|
||||
|
||||
// FormDataPdfStamp creates a [gotenberg.Stamp] for stamping from the form data.
|
||||
func FormDataPdfStamp(form *api.FormData, mandatory bool) gotenberg.Stamp {
|
||||
return formDataPdfStampOrWatermark(form, "stamp", mandatory)
|
||||
}
|
||||
|
||||
func formDataPdfStampOrWatermark(form *api.FormData, prefix string, mandatory bool) gotenberg.Stamp {
|
||||
var (
|
||||
source string
|
||||
expression string
|
||||
pages string
|
||||
options map[string]string
|
||||
)
|
||||
|
||||
sourceFunc := func(value string) error {
|
||||
if value != "" && value != gotenberg.StampSourceText && value != gotenberg.StampSourceImage && value != gotenberg.StampSourcePDF {
|
||||
return fmt.Errorf("wrong value, expected either '%s', '%s' or '%s'", gotenberg.StampSourceText, gotenberg.StampSourceImage, gotenberg.StampSourcePDF)
|
||||
}
|
||||
source = value
|
||||
return nil
|
||||
}
|
||||
|
||||
optionsFunc := func(value string) error {
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(value), &options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal %s options: %w", prefix, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if mandatory {
|
||||
form.
|
||||
MandatoryCustom(prefix+"Source", func(value string) error {
|
||||
return sourceFunc(value)
|
||||
}).
|
||||
String(prefix+"Expression", &expression, "").
|
||||
String(prefix+"Pages", &pages, "").
|
||||
Custom(prefix+"Options", func(value string) error {
|
||||
return optionsFunc(value)
|
||||
})
|
||||
} else {
|
||||
form.
|
||||
Custom(prefix+"Source", func(value string) error {
|
||||
return sourceFunc(value)
|
||||
}).
|
||||
String(prefix+"Expression", &expression, "").
|
||||
String(prefix+"Pages", &pages, "").
|
||||
Custom(prefix+"Options", func(value string) error {
|
||||
return optionsFunc(value)
|
||||
})
|
||||
}
|
||||
|
||||
return gotenberg.Stamp{
|
||||
Source: source,
|
||||
Expression: expression,
|
||||
Pages: pages,
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// FormDataPdfWatermarkFile extracts the watermark file path from form data.
|
||||
func FormDataPdfWatermarkFile(form *api.FormData) string {
|
||||
var path string
|
||||
form.Watermark(&path)
|
||||
return path
|
||||
}
|
||||
|
||||
// FormDataPdfStampFile extracts the stamp file path from form data.
|
||||
func FormDataPdfStampFile(form *api.FormData) string {
|
||||
var path string
|
||||
form.Stamp(&path)
|
||||
return path
|
||||
}
|
||||
|
||||
// WatermarkStub applies a watermark to a list of PDF files. If the stamp has
|
||||
// no source, it does nothing.
|
||||
func WatermarkStub(ctx *api.Context, engine gotenberg.PdfEngine, stamp gotenberg.Stamp, inputPaths []string) error {
|
||||
if stamp.Source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Watermark(ctx, ctx.Log(), inputPath, stamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("watermark '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StampStub applies a stamp to a list of PDF files. If the stamp has
|
||||
// no source, it does nothing.
|
||||
func StampStub(ctx *api.Context, engine gotenberg.PdfEngine, stamp gotenberg.Stamp, inputPaths []string) error {
|
||||
if stamp.Source == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.Stamp(ctx, ctx.Log(), inputPath, stamp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stamp '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeRoute returns an [api.Route] which can merge PDFs.
|
||||
func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
@@ -313,38 +604,60 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
form := ctx.FormData()
|
||||
pdfFormats := FormDataPdfFormats(form)
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
bookmarks := FormDataPdfBookmarks(form, false)
|
||||
userPassword, ownerPassword := FormDataPdfEncrypt(form)
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
watermark := FormDataPdfWatermark(form, false)
|
||||
watermarkFile := FormDataPdfWatermarkFile(form)
|
||||
stamp := FormDataPdfStamp(form, false)
|
||||
stampFile := FormDataPdfStampFile(form)
|
||||
angle, rotatePages := FormDataPdfRotate(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
var autoIndexBookmarks bool
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Bool("flatten", &flatten, false).
|
||||
Bool("autoIndexBookmarks", &autoIndexBookmarks, false).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && watermarkFile != "" {
|
||||
watermark.Expression = watermarkFile
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && stampFile != "" {
|
||||
stamp.Expression = stampFile
|
||||
}
|
||||
|
||||
err = ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputPath := ctx.GeneratePath(".pdf")
|
||||
err = engine.Merge(ctx, ctx.Log(), inputPaths, outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge PDFs: %w", err)
|
||||
}
|
||||
|
||||
outputPaths, err := ConvertStub(ctx, engine, pdfFormats, []string{outputPath})
|
||||
outputPaths := []string{outputPath}
|
||||
|
||||
err = WatermarkStub(ctx, engine, watermark, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
return fmt.Errorf("watermark PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
|
||||
err = StampStub(ctx, engine, stamp, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
return fmt.Errorf("stamp PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = WriteMetadataStub(ctx, engine, metadata, outputPaths)
|
||||
err = RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
return fmt.Errorf("rotate PDFs: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
@@ -354,6 +667,67 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
outputPaths, err = ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDF: %w", err)
|
||||
}
|
||||
|
||||
// Bookmarks, metadata, and embeds are written after Convert,
|
||||
// as LibreOffice strips them during PDF/A conversion.
|
||||
|
||||
var finalBookmarks []gotenberg.Bookmark
|
||||
if b, ok := bookmarks.([]gotenberg.Bookmark); ok {
|
||||
finalBookmarks = b
|
||||
} else {
|
||||
bMap, _ := bookmarks.(map[string][]gotenberg.Bookmark)
|
||||
if bMap != nil || autoIndexBookmarks {
|
||||
offset := 0
|
||||
for _, inputPath := range inputPaths {
|
||||
filename := filepath.Base(inputPath)
|
||||
|
||||
var fileBookmarks []gotenberg.Bookmark
|
||||
if bMap != nil {
|
||||
fileBookmarks = bMap[filename]
|
||||
}
|
||||
|
||||
if len(fileBookmarks) == 0 && autoIndexBookmarks {
|
||||
fb, err := engine.ReadBookmarks(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bookmarks of '%s': %w", filename, err)
|
||||
}
|
||||
fileBookmarks = fb
|
||||
}
|
||||
|
||||
if len(fileBookmarks) > 0 {
|
||||
finalBookmarks = append(finalBookmarks, shiftBookmarks(fileBookmarks, offset)...)
|
||||
}
|
||||
|
||||
pageCount, err := engine.PageCount(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get page count of '%s': %w", filename, err)
|
||||
}
|
||||
offset += pageCount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(finalBookmarks) > 0 {
|
||||
err = WriteBookmarksStub(ctx, engine, finalBookmarks, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bookmarks: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = WriteMetadataStub(ctx, engine, metadata, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
@@ -384,6 +758,11 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
metadata := FormDataPdfMetadata(form, false)
|
||||
userPassword, ownerPassword := FormDataPdfEncrypt(form)
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
watermark := FormDataPdfWatermark(form, false)
|
||||
watermarkFile := FormDataPdfWatermarkFile(form)
|
||||
stamp := FormDataPdfStamp(form, false)
|
||||
stampFile := FormDataPdfStampFile(form)
|
||||
angle, rotatePages := FormDataPdfRotate(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
@@ -395,31 +774,60 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
if (watermark.Source == gotenberg.StampSourceImage || watermark.Source == gotenberg.StampSourcePDF) && watermarkFile != "" {
|
||||
watermark.Expression = watermarkFile
|
||||
}
|
||||
if (stamp.Source == gotenberg.StampSourceImage || stamp.Source == gotenberg.StampSourcePDF) && stampFile != "" {
|
||||
stamp.Expression = stampFile
|
||||
}
|
||||
|
||||
err = ValidatePdfFormatsCompat(pdfFormats, userPassword, embedPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputPaths, err := SplitPdfStub(ctx, engine, mode, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("split PDFs: %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 = RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate PDFs: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = FlattenStub(ctx, engine, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
convertOutputPaths, err := ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("convert PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
// Metadata, embeds are written after Convert, as LibreOffice
|
||||
// strips them during PDF/A conversion.
|
||||
err = WriteMetadataStub(ctx, engine, metadata, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write metadata: %w", err)
|
||||
}
|
||||
|
||||
if flatten {
|
||||
err = FlattenStub(ctx, engine, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("flatten PDFs: %w", err)
|
||||
}
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
|
||||
@@ -619,6 +1027,83 @@ func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
}
|
||||
|
||||
// readBookmarksRoute returns an [api.Route] which returns the bookmarks of PDFs.
|
||||
func readBookmarksRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/bookmarks/read",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
var inputPaths []string
|
||||
err := ctx.FormData().
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
res := make(map[string][]gotenberg.Bookmark, len(inputPaths))
|
||||
for _, inputPath := range inputPaths {
|
||||
bookmarks, err := engine.ReadBookmarks(ctx, ctx.Log(), inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read bookmarks: %w", err)
|
||||
}
|
||||
|
||||
res[filepath.Base(inputPath)] = bookmarks
|
||||
}
|
||||
|
||||
err = c.JSON(http.StatusOK, res)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "request method or response status code does not allow body") {
|
||||
// High probability that the user is using the webhook
|
||||
// feature. It does not make sense for this route.
|
||||
return api.ErrNoOutputFile
|
||||
}
|
||||
return fmt.Errorf("return JSON response: %w", err)
|
||||
}
|
||||
|
||||
return api.ErrNoOutputFile
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// writeBookmarksRoute returns an [api.Route] which can write bookmarks into PDFs.
|
||||
func writeBookmarksRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/bookmarks/write",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
bookmarks := FormDataPdfBookmarks(form, true)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = WriteBookmarksStub(ctx, engine, bookmarks, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write bookmarks: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// encryptRoute returns an [api.Route] which can add password protection to PDFs.
|
||||
func encryptRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
@@ -658,6 +1143,7 @@ func encryptRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
}
|
||||
|
||||
// embedRoute returns an [api.Route] which can add embedded files to PDFs.
|
||||
// TODO: attachments instead?
|
||||
func embedRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
@@ -676,7 +1162,6 @@ func embedRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = EmbedFilesStub(ctx, engine, embedPaths, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("embed files into PDFs: %w", err)
|
||||
@@ -691,3 +1176,140 @@ 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)
|
||||
watermarkFile := FormDataPdfWatermarkFile(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 watermarkFile == "" {
|
||||
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 = watermarkFile
|
||||
}
|
||||
|
||||
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)
|
||||
stampFile := FormDataPdfStampFile(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 stampFile == "" {
|
||||
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 = stampFile
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// rotateRoute returns an [api.Route] which can rotate pages of PDFs.
|
||||
func rotateRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/rotate",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
angle, pages := FormDataPdfRotate(form, true)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = RotateStub(ctx, engine, angle, pages, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +145,21 @@ func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, meta
|
||||
return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// PageCount is not available in this implementation.
|
||||
func (engine *PdfTk) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
return 0, fmt.Errorf("page count with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteBookmarks is not available in this implementation.
|
||||
func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
return fmt.Errorf("write PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// ReadBookmarks is not available in this implementation.
|
||||
func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
return nil, fmt.Errorf("read PDF bookmarks with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using PDFtk.
|
||||
func (engine *PdfTk) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
if userPassword == "" {
|
||||
@@ -188,6 +203,105 @@ func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *zap.Logger, filePat
|
||||
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
|
||||
}
|
||||
|
||||
// Rotate rotates all pages of a PDF file by the given angle using PDFtk.
|
||||
// Page-specific rotation is not supported; if pages is non-empty,
|
||||
// ErrPdfEngineMethodNotSupported is returned.
|
||||
func (engine *PdfTk) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
if pages != "" {
|
||||
return fmt.Errorf("rotate PDF with PDFtk (page-specific rotation): %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
var direction string
|
||||
switch angle {
|
||||
case 90:
|
||||
direction = "east"
|
||||
case 180:
|
||||
direction = "south"
|
||||
case 270:
|
||||
direction = "west"
|
||||
default:
|
||||
return fmt.Errorf("rotate PDF with PDFtk: %w", gotenberg.ErrPdfRotateAngleNotSupported)
|
||||
}
|
||||
|
||||
tmpPath := inputPath + ".tmp"
|
||||
args := []string{inputPath, "cat", "1-end" + direction, "output", tmpPath}
|
||||
|
||||
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create command: %w", err)
|
||||
}
|
||||
|
||||
_, err = cmd.Exec()
|
||||
if err != nil {
|
||||
return fmt.Errorf("rotate PDF with PDFtk: %w", err)
|
||||
}
|
||||
|
||||
err = os.Rename(tmpPath, inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rename temporary output file with input file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.Module = (*PdfTk)(nil)
|
||||
|
||||
@@ -172,6 +172,21 @@ func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metad
|
||||
return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// PageCount is not available in this implementation.
|
||||
func (engine *QPdf) PageCount(ctx context.Context, logger *zap.Logger, inputPath string) (int, error) {
|
||||
return 0, fmt.Errorf("page count with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// WriteBookmarks is not available in this implementation.
|
||||
func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *zap.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
|
||||
return fmt.Errorf("write PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// ReadBookmarks is not available in this implementation.
|
||||
func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *zap.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
|
||||
return nil, fmt.Errorf("read PDF bookmarks with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
// Encrypt adds password protection to a PDF file using QPDF.
|
||||
func (engine *QPdf) Encrypt(ctx context.Context, logger *zap.Logger, inputPath, userPassword, ownerPassword string) error {
|
||||
if userPassword == "" {
|
||||
@@ -206,6 +221,21 @@ func (engine *QPdf) EmbedFiles(ctx context.Context, logger *zap.Logger, filePath
|
||||
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)
|
||||
}
|
||||
|
||||
// Rotate is not available in this implementation.
|
||||
func (engine *QPdf) Rotate(ctx context.Context, logger *zap.Logger, inputPath string, angle int, pages string) error {
|
||||
return fmt.Errorf("rotate PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported)
|
||||
}
|
||||
|
||||
var (
|
||||
_ gotenberg.Module = (*QPdf)(nil)
|
||||
_ gotenberg.Provisioner = (*QPdf)(nil)
|
||||
|
||||
@@ -988,6 +988,38 @@ Feature: /forms/chromium/convert/html
|
||||
Then there should be 1 PDF(s) in the response
|
||||
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
|
||||
|
||||
@rotate
|
||||
Scenario: POST /forms/chromium/convert/html (Rotate 90)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
|
||||
| files | testdata/page-1-html/index.html | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
|
||||
@embed
|
||||
Scenario: POST /forms/chromium/convert/html (Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
@@ -1007,13 +1039,15 @@ Feature: /forms/chromium/convert/html
|
||||
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
|
||||
@convert
|
||||
@metadata
|
||||
@watermark
|
||||
@stamp
|
||||
@flatten
|
||||
@embed
|
||||
Scenario: POST /forms/chromium/convert/html (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
Scenario: POST /forms/chromium/convert/html (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Flatten & Embeds)
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| pdfa | PDF/A-3b | 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 |
|
||||
| flatten | true | field |
|
||||
@@ -1025,8 +1059,8 @@ Feature: /forms/chromium/convert/html
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then there should be the following file(s) in the response:
|
||||
| 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/UA-1" with a tolerance of 2 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 5 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
|
||||
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_2.xml" file embedded
|
||||
@@ -1040,11 +1074,8 @@ Feature: /forms/chromium/convert/html
|
||||
"foo.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
|
||||
@@ -1012,12 +1012,12 @@ Feature: /forms/chromium/convert/markdown
|
||||
@metadata
|
||||
@flatten
|
||||
@embed
|
||||
Scenario: POST /forms/chromium/convert/markdown (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
Scenario: POST /forms/chromium/convert/markdown (PDF/A-3b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/markdown" endpoint with the following form data and header(s):
|
||||
| files | testdata/page-1-markdown/index.html | file |
|
||||
| files | testdata/page-1-markdown/page_1.md | file |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| pdfa | PDF/A-3b | 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 |
|
||||
| flatten | true | field |
|
||||
@@ -1029,8 +1029,8 @@ Feature: /forms/chromium/convert/markdown
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then there should be the following file(s) in the response:
|
||||
| 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/UA-1" with a tolerance of 2 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 5 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
|
||||
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_2.xml" file embedded
|
||||
@@ -1044,11 +1044,8 @@ Feature: /forms/chromium/convert/markdown
|
||||
"foo.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
|
||||
@@ -1107,12 +1107,12 @@ Feature: /forms/chromium/convert/url
|
||||
@metadata
|
||||
@flatten
|
||||
@embed
|
||||
Scenario: POST /forms/chromium/convert/url (PDF/A-1b & PDF/UA-1 & Metadata & Flatten)
|
||||
Scenario: POST /forms/chromium/convert/url (PDF/A-3b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s):
|
||||
| url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| pdfa | PDF/A-3b | 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 |
|
||||
| flatten | true | field |
|
||||
@@ -1124,8 +1124,8 @@ Feature: /forms/chromium/convert/url
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then there should be the following file(s) in the response:
|
||||
| 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/UA-1" with a tolerance of 2 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 5 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
|
||||
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_2.xml" file embedded
|
||||
@@ -1139,11 +1139,8 @@ Feature: /forms/chromium/convert/url
|
||||
"foo.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
|
||||
@@ -104,8 +104,10 @@ Feature: /debug
|
||||
"pdfengines-engines": "[]",
|
||||
"pdfengines-flatten-engines": "[qpdf]",
|
||||
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
||||
"pdfengines-read-bookmarks-engines": "[pdfcpu]",
|
||||
"pdfengines-read-metadata-engines": "[exiftool]",
|
||||
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
||||
"pdfengines-write-bookmarks-engines": "[pdfcpu]",
|
||||
"pdfengines-write-metadata-engines": "[exiftool]",
|
||||
"prometheus-collect-interval": "1s",
|
||||
"prometheus-disable-collect": "false",
|
||||
@@ -224,8 +226,10 @@ Feature: /debug
|
||||
"pdfengines-engines": "[]",
|
||||
"pdfengines-flatten-engines": "[qpdf]",
|
||||
"pdfengines-merge-engines": "[qpdf,pdfcpu,pdftk]",
|
||||
"pdfengines-read-bookmarks-engines": "[pdfcpu]",
|
||||
"pdfengines-read-metadata-engines": "[exiftool]",
|
||||
"pdfengines-split-engines": "[pdfcpu,qpdf,pdftk]",
|
||||
"pdfengines-write-bookmarks-engines": "[pdfcpu]",
|
||||
"pdfengines-write-metadata-engines": "[exiftool]",
|
||||
"prometheus-collect-interval": "1s",
|
||||
"prometheus-disable-collect": "false",
|
||||
|
||||
@@ -563,6 +563,72 @@ Feature: /forms/libreoffice/convert
|
||||
Then there should be 1 PDF(s) in the response
|
||||
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
|
||||
|
||||
@rotate
|
||||
Scenario: POST /forms/libreoffice/convert (Rotate 90)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.docx | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
|
||||
@watermark
|
||||
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
|
||||
Scenario: POST /forms/libreoffice/convert (Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
@@ -582,15 +648,23 @@ Feature: /forms/libreoffice/convert
|
||||
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
|
||||
@convert
|
||||
@metadata
|
||||
@watermark
|
||||
@stamp
|
||||
@rotate
|
||||
@flatten
|
||||
@embed
|
||||
Scenario: POST /forms/libreoffice/convert (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
Scenario: POST /forms/libreoffice/convert (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Rotate & Flatten & Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| pdfa | PDF/A-3b | 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 |
|
||||
| watermarkSource | text | field |
|
||||
| watermarkExpression | CONFIDENTIAL | field |
|
||||
| stampSource | text | field |
|
||||
| stampExpression | DRAFT | field |
|
||||
| rotateAngle | 90 | field |
|
||||
| flatten | true | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
| embeds | testdata/embed_2.xml | file |
|
||||
@@ -600,8 +674,8 @@ Feature: /forms/libreoffice/convert
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then there should be the following file(s) in the response:
|
||||
| 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/UA-1" with a tolerance of 2 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 5 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
|
||||
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_2.xml" file embedded
|
||||
@@ -615,11 +689,8 @@ Feature: /forms/libreoffice/convert
|
||||
"foo.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
|
||||
339
test/integration/features/pdfengines_bookmarks.feature
Normal file
339
test/integration/features/pdfengines_bookmarks.feature
Normal file
@@ -0,0 +1,339 @@
|
||||
@pdfengines
|
||||
@pdfengines-bookmarks
|
||||
@bookmarks
|
||||
Feature: /forms/pdfengines/bookmarks/{write|read}
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/{write|read} (Single PDF & Bookmarks list)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1,"children":[{"title":"Sub-index","page":1}]}] | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
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
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1,
|
||||
"children": [
|
||||
{
|
||||
"title": "Sub-index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/{write|read} (Single PDF & Bookmarks Map)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | {"page_1.pdf":[{"title":"Index","page":1}]} | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
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
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/{write|read} (Many PDFs & Bookmarks List)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files. | testdata/page_1.pdf | file |
|
||||
| files | testdata/page_2.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | 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
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/page_1.pdf | file |
|
||||
| files | teststore/page_2.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"page_1.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1
|
||||
}
|
||||
],
|
||||
"page_2.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/{write|read} (Many PDFs & Bookmarks Map)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files. | testdata/page_1.pdf | file |
|
||||
| files | testdata/page_2.pdf | file |
|
||||
| bookmarks | {"page_1.pdf":[{"title":"Index","page":1}],"page_2.pdf":[{"title":"Index","page":1}]} | 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
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/page_1.pdf | file |
|
||||
| files | teststore/page_2.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"page_1.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1
|
||||
}
|
||||
],
|
||||
"page_2.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (Empty List)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"page_1.pdf": []
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (Bad Request)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: form field 'bookmarks' is required; no form file found for extensions: [.pdf]
|
||||
"""
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | foo | field |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: form field 'bookmarks' is invalid (got 'foo', resulting to unmarshal bookmarks: invalid character 'o' in literal false (expecting 'a'))
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (Bad Request)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: no form file found for extensions: [.pdf]
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (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/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
Then the response status code should be 404
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (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/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
Then the response status code should be 404
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (Gotenberg Trace)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
| Gotenberg-Trace | forms_pdfengines_bookmarks_write | 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_bookmarks_write"
|
||||
Then the Gotenberg container should log the following entries:
|
||||
| "trace":"forms_pdfengines_bookmarks_write" |
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (Gotenberg Trace)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| Gotenberg-Trace | forms_pdfengines_bookmarks_read | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response header "Gotenberg-Trace" should be "forms_pdfengines_bookmarks_read"
|
||||
Then the Gotenberg container should log the following entries:
|
||||
| "trace":"forms_pdfengines_bookmarks_read" |
|
||||
|
||||
@output-filename
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (Output Filename - Single PDF)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be the following file(s) in the response:
|
||||
| foo.pdf |
|
||||
|
||||
@output-filename
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (Output Filename - Many PDFs)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| files | testdata/page_2.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/zip"
|
||||
Then there should be the following file(s) in the response:
|
||||
| foo.zip |
|
||||
| page_1.pdf |
|
||||
| page_2.pdf |
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (Download From)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the file request header "X-Foo" should be "bar"
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (Download From)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a static server
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_1.pdf","extraHttpHeaders":{"X-Foo":"bar"}}] | field |
|
||||
Then the file request header "X-Foo" should be "bar"
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
|
||||
@webhook
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (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/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
| Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header |
|
||||
| Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header |
|
||||
| Gotenberg-Output-Filename | foo | 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
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
@webhook
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (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/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| 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/json"
|
||||
Then the webhook request body should match JSON:
|
||||
"""
|
||||
{
|
||||
"status": 400,
|
||||
"message": "The webhook middleware can only work with multipart/form-data routes that results in output files"
|
||||
}
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/write (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/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
Then the response status code should be 401
|
||||
|
||||
Scenario: POST /forms/pdfengines/bookmarks/read (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/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
Then the response status code should be 401
|
||||
|
||||
Scenario: POST /foo/forms/pdfengines/bookmarks/{write|read} (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/bookmarks/write" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| bookmarks | [{"title":"Index","page":1}] | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
@@ -1,5 +1,5 @@
|
||||
@pdfengines
|
||||
@pdfengines-encrypt
|
||||
@pdfengines-merge
|
||||
@merge
|
||||
Feature: /forms/pdfengines/merge
|
||||
|
||||
@@ -130,6 +130,26 @@ Feature: /forms/pdfengines/merge
|
||||
"""
|
||||
Invalid form data: form field 'metadata' is invalid (got 'foo', resulting to unmarshal metadata: invalid character 'o' in literal false (expecting 'a'))
|
||||
"""
|
||||
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 |
|
||||
| bookmarks | foo | field |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: form field 'bookmarks' is invalid (got 'foo', resulting to unmarshal bookmarks: invalid character 'o' in literal false (expecting 'a'))
|
||||
"""
|
||||
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 |
|
||||
| autoIndexBookmarks | foo | field |
|
||||
Then the response status code should be 400
|
||||
Then the response header "Content-Type" should be "text/plain; charset=UTF-8"
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: form field 'autoIndexBookmarks' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax)
|
||||
"""
|
||||
|
||||
@convert
|
||||
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1)
|
||||
@@ -203,6 +223,135 @@ Feature: /forms/pdfengines/merge
|
||||
}
|
||||
"""
|
||||
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (Bookmarks List)
|
||||
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 |
|
||||
| bookmarks | [{"title":"Merged Index","page":1}] | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Merged Index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (Bookmarks Map)
|
||||
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 |
|
||||
| bookmarks | {"page_1.pdf":[{"title":"Page 1 Index","page":1,"children":[{"title":"Page 1 Sub-index","page":1}]}],"page_2.pdf":[{"title":"Page 2 Index","page":1,"children":[{"title":"Page 2 Sub-index","page":1}]}]} | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Page 1 Index",
|
||||
"page": 1,
|
||||
"children": [
|
||||
{
|
||||
"title": "Page 1 Sub-index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Page 2 Index",
|
||||
"page": 2,
|
||||
"children": [
|
||||
{
|
||||
"title": "Page 2 Sub-index",
|
||||
"page": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (Auto-index Bookmarks)
|
||||
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_with_bookmarks.pdf | file |
|
||||
| files | testdata/page_2_with_bookmarks.pdf | file |
|
||||
| autoIndexBookmarks | true | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Page 1",
|
||||
"page": 1
|
||||
},
|
||||
{
|
||||
"title": "Page 2",
|
||||
"page": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (Auto-index Bookmarks + Bookmarks Map)
|
||||
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_with_bookmarks.pdf | file |
|
||||
| bookmarks | {"page_1.pdf":[{"title":"Page 1 Index","page":1}]} | field |
|
||||
| autoIndexBookmarks | true | field |
|
||||
| Gotenberg-Output-Filename | foo | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Page 1 Index",
|
||||
"page": 1
|
||||
},
|
||||
{
|
||||
"title": "Page 2",
|
||||
"page": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
@flatten
|
||||
Scenario: POST /forms/pdfengines/merge (Flatten)
|
||||
Given I have a default Gotenberg container
|
||||
@@ -252,6 +401,41 @@ Feature: /forms/pdfengines/merge
|
||||
Then there should be 1 PDF(s) in the response
|
||||
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
|
||||
|
||||
@rotate
|
||||
Scenario: POST /forms/pdfengines/merge (Rotate 90)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/merge" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| files | testdata/page_2.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
|
||||
@embed
|
||||
Scenario: POST /foo/forms/pdfengines/merge (Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
@@ -268,16 +452,26 @@ Feature: /forms/pdfengines/merge
|
||||
# FIXME: once decrypt is done, add encrypt and check after the content of the PDF.
|
||||
@convert
|
||||
@metadata
|
||||
@watermark
|
||||
@stamp
|
||||
@rotate
|
||||
@flatten
|
||||
@embed
|
||||
Scenario: POST /forms/pdfengines/merge (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
@bookmarks
|
||||
Scenario: POST /forms/pdfengines/merge (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Rotate & Flatten & Embeds & Bookmarks)
|
||||
Given I have a default Gotenberg container
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| pdfa | PDF/A-3b | 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 |
|
||||
| watermarkSource | text | field |
|
||||
| watermarkExpression | CONFIDENTIAL | field |
|
||||
| stampSource | text | field |
|
||||
| stampExpression | DRAFT | field |
|
||||
| rotateAngle | 90 | field |
|
||||
| bookmarks | [{"title":"Merged Index","page":1}] | field |
|
||||
| flatten | true | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
| embeds | testdata/embed_2.xml | file |
|
||||
@@ -296,11 +490,26 @@ Feature: /forms/pdfengines/merge
|
||||
"""
|
||||
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/UA-1" with a tolerance of 2 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 5 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
|
||||
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_2.xml" file embedded
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/bookmarks/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/json"
|
||||
Then the response body should match JSON:
|
||||
"""
|
||||
{
|
||||
"foo.pdf": [
|
||||
{
|
||||
"title": "Merged Index",
|
||||
"page": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/metadata/read" endpoint with the following form data and header(s):
|
||||
| files | teststore/foo.pdf | file |
|
||||
Then the response status code should be 200
|
||||
@@ -311,11 +520,8 @@ Feature: /forms/pdfengines/merge
|
||||
"foo.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
@@ -354,6 +560,32 @@ Feature: /forms/pdfengines/merge
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/merge (Watermark via Download From)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
|
||||
Given I have a static server
|
||||
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 |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/watermark.png","field":"watermark"}] | field |
|
||||
| watermarkSource | image | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/merge (Stamp via Download From)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_STAMP_ENGINES | pdfcpu |
|
||||
Given I have a static server
|
||||
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 |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_2.pdf","field":"stamp"}] | field |
|
||||
| stampSource | pdf | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
|
||||
@webhook
|
||||
Scenario: POST /forms/pdfengines/merge (Webhook)
|
||||
Given I have a default Gotenberg container
|
||||
@@ -399,3 +631,36 @@ Feature: /forms/pdfengines/merge
|
||||
| files | testdata/page_2.pdf | file |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
|
||||
@convert
|
||||
@encrypt
|
||||
Scenario: POST /forms/pdfengines/merge (PDF/A + Encrypt => 400)
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| userPassword | secret | field |
|
||||
Then the response status code should be 400
|
||||
|
||||
@convert
|
||||
@embed
|
||||
Scenario: POST /forms/pdfengines/merge (PDF/A-1b + Embeds => 400)
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
Then the response status code should be 400
|
||||
|
||||
@convert
|
||||
@embed
|
||||
Scenario: POST /forms/pdfengines/merge (PDF/A-3b + Embeds => 200)
|
||||
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 |
|
||||
| pdfa | PDF/A-3b | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
Then the response status code should be 200
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@pdfengines
|
||||
@pdfengines-metadata
|
||||
@metadata
|
||||
Feature: /forms/pdfengines/{write|read}
|
||||
Feature: /forms/pdfengines/metadata/{write|read}
|
||||
|
||||
Scenario: POST /forms/pdfengines/metadata/{write|read} (Single PDF)
|
||||
Given I have a default Gotenberg container
|
||||
|
||||
164
test/integration/features/pdfengines_rotate.feature
Normal file
164
test/integration/features/pdfengines_rotate.feature
Normal file
@@ -0,0 +1,164 @@
|
||||
@pdfengines
|
||||
@pdfengines-rotate
|
||||
@rotate
|
||||
Feature: /forms/pdfengines/rotate
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (90 - All Pages - pdfcpu)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/pages_3.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "pages_3.pdf" PDF should have 3 page(s)
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (180 - All Pages - pdfcpu)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 180 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "page_1.pdf" PDF should have 1 page(s)
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (270 - All Pages - pdfcpu)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 270 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "page_1.pdf" PDF should have 1 page(s)
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (90 - Specific Pages - pdfcpu)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_ROTATE_ENGINES | pdfcpu |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/pages_3.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
| rotatePages | 1,3 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "pages_3.pdf" PDF should have 3 page(s)
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (90 - All Pages - pdftk)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_ROTATE_ENGINES | pdftk |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the response
|
||||
Then the "page_1.pdf" PDF should have 1 page(s)
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Specific Pages - pdftk unsupported)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_ROTATE_ENGINES | pdftk |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/pages_3.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
| rotatePages | 1,3 | field |
|
||||
Then the response status code should be 500
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Bad Request - Invalid Angle)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 45 | field |
|
||||
Then the response status code should be 400
|
||||
Then the response body should contain string:
|
||||
"""
|
||||
Invalid form data: form field 'rotateAngle' is invalid
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Bad Request - Missing Angle)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
Then the response status code should be 400
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: form field 'rotateAngle' is required
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Bad Request - No PDF)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 400
|
||||
Then the response body should match string:
|
||||
"""
|
||||
Invalid form data: no form file found for extensions: [.pdf]
|
||||
"""
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Many PDFs)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| files | testdata/page_2.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/zip"
|
||||
Then there should be 2 PDF(s) in the response
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Routes Disabled)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_DISABLE_ROUTES | true |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 404
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Gotenberg Trace)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
| Gotenberg-Trace | forms_pdfengines_rotate | header |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
Then the response header "Gotenberg-Trace" should be "forms_pdfengines_rotate"
|
||||
Then the Gotenberg container should log the following entries:
|
||||
| "trace":"forms_pdfengines_rotate" |
|
||||
|
||||
@webhook
|
||||
Scenario: POST /forms/pdfengines/rotate (Webhook)
|
||||
Given I have a default Gotenberg container
|
||||
Given I have a webhook server
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
| Gotenberg-Webhook-Url | http://host.docker.internal:%d/webhook | header |
|
||||
| Gotenberg-Webhook-Error-Url | http://host.docker.internal:%d/webhook/error | header |
|
||||
Then the response status code should be 204
|
||||
When I wait for the asynchronous request to the webhook
|
||||
Then the webhook request header "Content-Type" should be "application/pdf"
|
||||
Then there should be 1 PDF(s) in the webhook request
|
||||
|
||||
Scenario: POST /forms/pdfengines/rotate (Basic Auth)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| API_ENABLE_BASIC_AUTH | true |
|
||||
| GOTENBERG_API_BASIC_AUTH_USERNAME | foo |
|
||||
| GOTENBERG_API_BASIC_AUTH_PASSWORD | bar |
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 401
|
||||
|
||||
Scenario: POST /foo/forms/pdfengines/rotate (Root Path)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| API_ENABLE_DEBUG_ROUTE | true |
|
||||
| API_ROOT_PATH | /foo/ |
|
||||
When I make a "POST" request to Gotenberg at the "/foo/forms/pdfengines/rotate" endpoint with the following form data and header(s):
|
||||
| files | testdata/page_1.pdf | file |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/pdf"
|
||||
@@ -473,6 +473,44 @@ Feature: /forms/pdfengines/split
|
||||
Then there should be 2 PDF(s) in the response
|
||||
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
|
||||
|
||||
@rotate
|
||||
Scenario: POST /forms/pdfengines/split (Rotate 90)
|
||||
Given I have a default Gotenberg container
|
||||
When I make a "POST" request to Gotenberg at the "/forms/pdfengines/split" endpoint with the following form data and header(s):
|
||||
| files | testdata/pages_3.pdf | file |
|
||||
| splitMode | intervals | field |
|
||||
| splitSpan | 2 | field |
|
||||
| rotateAngle | 90 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/zip"
|
||||
Then there should be 2 PDF(s) in the response
|
||||
|
||||
@embed
|
||||
Scenario: POST /foo/forms/pdfengines/split (Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
@@ -494,20 +532,28 @@ Feature: /forms/pdfengines/split
|
||||
# FIXME: once decrypt is done, add encrypt and check after the content of the PDFs.
|
||||
@convert
|
||||
@metadata
|
||||
@watermark
|
||||
@stamp
|
||||
@rotate
|
||||
@flatten
|
||||
@embed
|
||||
Scenario: POST /forms/pdfengines/split (PDF/A-1b & PDF/UA-1 & Metadata & Flatten & Embeds)
|
||||
Scenario: POST /forms/pdfengines/split (PDF/A-3b & PDF/UA-1 & Metadata & Watermark & Stamp & Rotate & Flatten & Embeds)
|
||||
Given I have a default Gotenberg container
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | 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 |
|
||||
| flatten | true | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
| embeds | testdata/embed_2.xml | file |
|
||||
| files | testdata/pages_3.pdf | file |
|
||||
| splitMode | intervals | field |
|
||||
| splitSpan | 2 | field |
|
||||
| pdfa | PDF/A-3b | 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 |
|
||||
| watermarkSource | text | field |
|
||||
| watermarkExpression | CONFIDENTIAL | field |
|
||||
| stampSource | text | field |
|
||||
| stampExpression | DRAFT | field |
|
||||
| rotateAngle | 90 | 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 header "Content-Type" should be "application/zip"
|
||||
Then there should be 2 PDF(s) in the response
|
||||
@@ -528,8 +574,8 @@ Feature: /forms/pdfengines/split
|
||||
"""
|
||||
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/UA-1" with a tolerance of 2 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/A-3b" with a tolerance of 5 failed rule(s)
|
||||
Then the response PDF(s) should be valid "PDF/UA-1" with a tolerance of 3 failed rule(s)
|
||||
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_2.xml" file embedded
|
||||
@@ -544,11 +590,8 @@ Feature: /forms/pdfengines/split
|
||||
"pages_3_0.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
@@ -558,11 +601,8 @@ Feature: /forms/pdfengines/split
|
||||
"pages_3_1.pdf": {
|
||||
"Author": "Julien Neuhart",
|
||||
"Copyright": "Julien Neuhart",
|
||||
"CreateDate": "2006:09:18 16:27:50-04:00",
|
||||
"Creator": "Gotenberg",
|
||||
"Keywords": ["first", "second"],
|
||||
"Marked": true,
|
||||
"ModDate": "2006:09:18 16:27:50-04:00",
|
||||
"PDFVersion": 1.7,
|
||||
"Producer": "Gotenberg",
|
||||
"Subject": "Sample",
|
||||
@@ -688,3 +728,39 @@ Feature: /forms/pdfengines/split
|
||||
| splitSpan | 2 | field |
|
||||
Then the response status code should be 200
|
||||
Then the response header "Content-Type" should be "application/zip"
|
||||
|
||||
@convert
|
||||
@encrypt
|
||||
Scenario: POST /forms/pdfengines/split (PDF/A + Encrypt => 400)
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| userPassword | secret | field |
|
||||
Then the response status code should be 400
|
||||
|
||||
@convert
|
||||
@embed
|
||||
Scenario: POST /forms/pdfengines/split (PDF/A-1b + Embeds => 400)
|
||||
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 |
|
||||
| pdfa | PDF/A-1b | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
Then the response status code should be 400
|
||||
|
||||
@convert
|
||||
@embed
|
||||
Scenario: POST /forms/pdfengines/split (PDF/A-3b + Embeds => 200)
|
||||
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 |
|
||||
| pdfa | PDF/A-3b | field |
|
||||
| embeds | testdata/embed_1.xml | file |
|
||||
Then the response status code should be 200
|
||||
|
||||
237
test/integration/features/pdfengines_stamp.feature
Normal file
237
test/integration/features/pdfengines_stamp.feature
Normal file
@@ -0,0 +1,237 @@
|
||||
@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 |
|
||||
| stamp | 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 |
|
||||
| stamp | 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 |
|
||||
| stamp | 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
|
||||
"""
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/stamp (Image via Download From)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_STAMP_ENGINES | pdfcpu |
|
||||
Given I have a static 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 |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/watermark.png","field":"stamp"}] | field |
|
||||
| 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
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/stamp (PDF via Download From)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_STAMP_ENGINES | pdfcpu |
|
||||
Given I have a static 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 |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_2.pdf","field":"stamp"}] | field |
|
||||
| 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 (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"
|
||||
237
test/integration/features/pdfengines_watermark.feature
Normal file
237
test/integration/features/pdfengines_watermark.feature
Normal file
@@ -0,0 +1,237 @@
|
||||
@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 |
|
||||
| watermark | 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 |
|
||||
| watermark | 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 |
|
||||
| watermark | 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
|
||||
"""
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/watermark (Image via Download From)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
|
||||
Given I have a static 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 |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/watermark.png","field":"watermark"}] | field |
|
||||
| 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
|
||||
|
||||
@download-from
|
||||
Scenario: POST /forms/pdfengines/watermark (PDF via Download From)
|
||||
Given I have a Gotenberg container with the following environment variable(s):
|
||||
| PDFENGINES_WATERMARK_ENGINES | pdfcpu |
|
||||
Given I have a static 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 |
|
||||
| downloadFrom | [{"url":"http://host.docker.internal:%d/static/testdata/page_2.pdf","field":"watermark"}] | field |
|
||||
| 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 (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"
|
||||
@@ -27,6 +27,7 @@ type scenario struct {
|
||||
resp *httptest.ResponseRecorder
|
||||
concurrentResps []*httptest.ResponseRecorder
|
||||
workdir string
|
||||
teststoreDir string
|
||||
gotenbergContainer testcontainers.Container
|
||||
gotenbergContainerNetwork *testcontainers.DockerNetwork
|
||||
server *server
|
||||
@@ -163,12 +164,14 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
||||
fields[name] = value
|
||||
case "file":
|
||||
if strings.Contains(value, "teststore") {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", dirPath)
|
||||
if s.teststoreDir == "" {
|
||||
return errors.New("no teststore directory available from previous requests")
|
||||
}
|
||||
value = strings.ReplaceAll(value, "teststore", dirPath)
|
||||
_, err := os.Stat(s.teststoreDir)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("directory %q does not exist", s.teststoreDir)
|
||||
}
|
||||
value = strings.ReplaceAll(value, "teststore", s.teststoreDir)
|
||||
} else {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@@ -216,6 +219,13 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
||||
return fmt.Errorf("write response body: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNoContent {
|
||||
// Gotenberg processes this asynchronously. The webhook test server
|
||||
// will save the incoming files under this trace ID directory shortly.
|
||||
s.teststoreDir = fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
@@ -241,6 +251,8 @@ func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders(ct
|
||||
return fmt.Errorf("create working directory: %w", err)
|
||||
}
|
||||
|
||||
s.teststoreDir = dirPath
|
||||
|
||||
fpath := fmt.Sprintf("%s/%s", dirPath, filename)
|
||||
file, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
@@ -637,7 +649,7 @@ func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *godog.DocStr
|
||||
}
|
||||
|
||||
func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
dirPath := s.teststoreDir
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -666,7 +678,7 @@ func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
|
||||
}
|
||||
|
||||
func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTable *godog.Table) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
dirPath := s.teststoreDir
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -708,7 +720,7 @@ func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTable *godog
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Context, kind, validate string, tolerance int) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
dirPath := s.teststoreDir
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -788,7 +800,7 @@ func (s *scenario) thePdfShouldHavePages(ctx context.Context, name string, pages
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
@@ -844,7 +856,7 @@ func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context,
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
@@ -911,7 +923,7 @@ func (s *scenario) thePdfShouldHaveTheFollowingContentAtPage(ctx context.Context
|
||||
}
|
||||
} else {
|
||||
substr := strings.ReplaceAll(name, "*_", "")
|
||||
err := filepath.Walk(fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace")), func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
err := filepath.Walk(s.teststoreDir, func(currentPath string, info os.FileInfo, pathErr error) error {
|
||||
if pathErr != nil {
|
||||
return pathErr
|
||||
}
|
||||
@@ -955,7 +967,7 @@ func (s *scenario) thePdfShouldHaveTheFollowingContentAtPage(ctx context.Context
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should string) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
dirPath := s.teststoreDir
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -1005,7 +1017,7 @@ func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, should stri
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind string, should string) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
dirPath := s.teststoreDir
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
@@ -1057,7 +1069,7 @@ func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind string, sh
|
||||
}
|
||||
|
||||
func (s *scenario) thePdfsShouldHaveEmbeddedFile(ctx context.Context, kind, should, embed string) error {
|
||||
dirPath := fmt.Sprintf("%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"))
|
||||
dirPath := s.teststoreDir
|
||||
|
||||
_, err := os.Stat(dirPath)
|
||||
if os.IsNotExist(err) {
|
||||
|
||||
BIN
test/integration/testdata/page_1_with_bookmarks.pdf
vendored
Normal file
BIN
test/integration/testdata/page_1_with_bookmarks.pdf
vendored
Normal file
Binary file not shown.
BIN
test/integration/testdata/page_2_with_bookmarks.pdf
vendored
Normal file
BIN
test/integration/testdata/page_2_with_bookmarks.pdf
vendored
Normal file
Binary file not shown.
BIN
test/integration/testdata/watermark.png
vendored
Normal file
BIN
test/integration/testdata/watermark.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 B |
Reference in New Issue
Block a user