Showing preview only (1,262K chars total). Download the full file or copy to clipboard to get everything.
Repository: gotenberg/gotenberg
Branch: main
Commit: 3b0eb069911a
Files: 274
Total size: 1.2 MB
Directory structure:
gitextract_bfoz4ih4/
├── .bruno/
│ ├── Chromium/
│ │ ├── Convert/
│ │ │ ├── HTML to PDF.bru
│ │ │ ├── Markdown to PDF.bru
│ │ │ └── URL to PDF.bru
│ │ └── Screenshot/
│ │ ├── HTML Screenshot.bru
│ │ ├── Markdown Screenshot.bru
│ │ └── URL Screenshot.bru
│ ├── Health & Info/
│ │ ├── Debug.bru
│ │ ├── Health.bru
│ │ ├── Prometheus Metrics.bru
│ │ └── Version.bru
│ ├── LibreOffice/
│ │ └── Convert to PDF.bru
│ ├── PDF Engines/
│ │ ├── Bookmarks/
│ │ │ ├── Read Bookmarks.bru
│ │ │ └── Write Bookmarks.bru
│ │ ├── Convert/
│ │ │ └── Convert PDF.bru
│ │ ├── Embed/
│ │ │ └── Embed Files.bru
│ │ ├── Encrypt/
│ │ │ └── Encrypt PDF.bru
│ │ ├── Flatten/
│ │ │ └── Flatten PDF.bru
│ │ ├── Merge/
│ │ │ └── Merge PDFs.bru
│ │ ├── Metadata/
│ │ │ ├── Read Metadata.bru
│ │ │ └── Write Metadata.bru
│ │ ├── Rotate/
│ │ │ └── Rotate PDF.bru
│ │ ├── Split/
│ │ │ └── Split PDF.bru
│ │ ├── Stamp/
│ │ │ └── Stamp PDF.bru
│ │ └── Watermark/
│ │ └── Watermark PDF.bru
│ ├── bruno.json
│ ├── collection.bru
│ └── environments/
│ ├── Demo.bru
│ └── Local.bru
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── actions/
│ │ ├── build-test-push/
│ │ │ ├── action.yml
│ │ │ ├── build.sh
│ │ │ ├── push.sh
│ │ │ └── test.sh
│ │ ├── clean/
│ │ │ ├── action.yml
│ │ │ └── clean.sh
│ │ └── merge/
│ │ ├── action.yml
│ │ └── merge.sh
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ ├── continuous-delivery.yml
│ ├── continuous-integration.yml
│ └── pull-request-cleanup.yml
├── .gitignore
├── .golangci.yml
├── .node-version
├── .prettierignore
├── .prettierrc
├── AGENTS.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── GEMINI.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── build/
│ ├── Dockerfile
│ ├── Dockerfile.aws-lambda
│ ├── Dockerfile.cloudrun
│ ├── chromium-hyphen-data/
│ │ └── 120.0.6050.0/
│ │ ├── _metadata/
│ │ │ └── verified_contents.json
│ │ ├── hyph-af.hyb
│ │ ├── hyph-as.hyb
│ │ ├── hyph-be.hyb
│ │ ├── hyph-bg.hyb
│ │ ├── hyph-bn.hyb
│ │ ├── hyph-cs.hyb
│ │ ├── hyph-cu.hyb
│ │ ├── hyph-cy.hyb
│ │ ├── hyph-da.hyb
│ │ ├── hyph-de-1901.hyb
│ │ ├── hyph-de-1996.hyb
│ │ ├── hyph-de-ch-1901.hyb
│ │ ├── hyph-el.hyb
│ │ ├── hyph-en-gb.hyb
│ │ ├── hyph-en-us.hyb
│ │ ├── hyph-es.hyb
│ │ ├── hyph-et.hyb
│ │ ├── hyph-eu.hyb
│ │ ├── hyph-fr.hyb
│ │ ├── hyph-ga.hyb
│ │ ├── hyph-gl.hyb
│ │ ├── hyph-gu.hyb
│ │ ├── hyph-hi.hyb
│ │ ├── hyph-hr.hyb
│ │ ├── hyph-hu.hyb
│ │ ├── hyph-hy.hyb
│ │ ├── hyph-it.hyb
│ │ ├── hyph-ka.hyb
│ │ ├── hyph-kn.hyb
│ │ ├── hyph-la.hyb
│ │ ├── hyph-lt.hyb
│ │ ├── hyph-lv.hyb
│ │ ├── hyph-ml.hyb
│ │ ├── hyph-mn-cyrl.hyb
│ │ ├── hyph-mr.hyb
│ │ ├── hyph-mul-ethi.hyb
│ │ ├── hyph-nb.hyb
│ │ ├── hyph-nl.hyb
│ │ ├── hyph-nn.hyb
│ │ ├── hyph-or.hyb
│ │ ├── hyph-pa.hyb
│ │ ├── hyph-pt.hyb
│ │ ├── hyph-ru.hyb
│ │ ├── hyph-sk.hyb
│ │ ├── hyph-sl.hyb
│ │ ├── hyph-sq.hyb
│ │ ├── hyph-sv.hyb
│ │ ├── hyph-ta.hyb
│ │ ├── hyph-te.hyb
│ │ ├── hyph-tk.hyb
│ │ ├── hyph-uk.hyb
│ │ ├── hyph-und-ethi.hyb
│ │ └── manifest.json
│ └── fonts.conf
├── cmd/
│ ├── gotenberg/
│ │ └── main.go
│ └── gotenberg.go
├── go.mod
├── go.sum
├── package.json
├── pkg/
│ ├── gotenberg/
│ │ ├── cmd.go
│ │ ├── context.go
│ │ ├── context_test.go
│ │ ├── debug.go
│ │ ├── debug_test.go
│ │ ├── doc.go
│ │ ├── env.go
│ │ ├── env_test.go
│ │ ├── filter.go
│ │ ├── filter_test.go
│ │ ├── flags.go
│ │ ├── flags_test.go
│ │ ├── fs.go
│ │ ├── gc.go
│ │ ├── gc_test.go
│ │ ├── logging.go
│ │ ├── metrics.go
│ │ ├── mocks.go
│ │ ├── modules.go
│ │ ├── modules_test.go
│ │ ├── pdfengine.go
│ │ ├── shutdown.go
│ │ ├── sort.go
│ │ ├── sort_test.go
│ │ ├── supervisor.go
│ │ ├── supervisor_test.go
│ │ └── version.go
│ ├── modules/
│ │ ├── api/
│ │ │ ├── api.go
│ │ │ ├── context.go
│ │ │ ├── context_test.go
│ │ │ ├── doc.go
│ │ │ ├── errors.go
│ │ │ ├── errors_test.go
│ │ │ ├── formdata.go
│ │ │ ├── formdata_test.go
│ │ │ ├── middlewares.go
│ │ │ ├── mocks.go
│ │ │ └── testdata/
│ │ │ └── sample.txt
│ │ ├── chromium/
│ │ │ ├── browser.go
│ │ │ ├── chromium.go
│ │ │ ├── debug.go
│ │ │ ├── doc.go
│ │ │ ├── events.go
│ │ │ ├── events_test.go
│ │ │ ├── mocks.go
│ │ │ ├── routes.go
│ │ │ ├── stream.go
│ │ │ └── tasks.go
│ │ ├── exiftool/
│ │ │ ├── doc.go
│ │ │ └── exiftool.go
│ │ ├── libreoffice/
│ │ │ ├── api/
│ │ │ │ ├── api.go
│ │ │ │ ├── doc.go
│ │ │ │ ├── freeport.go
│ │ │ │ ├── libreoffice.go
│ │ │ │ └── mocks.go
│ │ │ ├── doc.go
│ │ │ ├── libreoffice.go
│ │ │ ├── pdfengine/
│ │ │ │ ├── doc.go
│ │ │ │ └── pdfengine.go
│ │ │ └── routes.go
│ │ ├── logging/
│ │ │ ├── color.go
│ │ │ ├── doc.go
│ │ │ ├── gcp.go
│ │ │ └── logging.go
│ │ ├── pdfcpu/
│ │ │ ├── doc.go
│ │ │ ├── pdfcpu.go
│ │ │ ├── sort.go
│ │ │ └── sort_test.go
│ │ ├── pdfengines/
│ │ │ ├── doc.go
│ │ │ ├── multi.go
│ │ │ ├── pdfengines.go
│ │ │ └── routes.go
│ │ ├── pdftk/
│ │ │ ├── doc.go
│ │ │ └── pdftk.go
│ │ ├── prometheus/
│ │ │ ├── doc.go
│ │ │ └── prometheus.go
│ │ ├── qpdf/
│ │ │ ├── doc.go
│ │ │ └── qpdf.go
│ │ └── webhook/
│ │ ├── client.go
│ │ ├── doc.go
│ │ ├── middleware.go
│ │ └── webhook.go
│ └── standard/
│ ├── doc.go
│ └── imports.go
└── test/
└── integration/
├── doc.go
├── features/
│ ├── chromium_concurrent.feature
│ ├── chromium_convert_html.feature
│ ├── chromium_convert_markdown.feature
│ ├── chromium_convert_url.feature
│ ├── debug.feature
│ ├── health.feature
│ ├── libreoffice_convert.feature
│ ├── output_filename.feature
│ ├── pdfengines_bookmarks.feature
│ ├── pdfengines_convert.feature
│ ├── pdfengines_embed.feature
│ ├── pdfengines_encrypt.feature
│ ├── pdfengines_flatten.feature
│ ├── pdfengines_merge.feature
│ ├── pdfengines_metadata.feature
│ ├── pdfengines_rotate.feature
│ ├── pdfengines_split.feature
│ ├── pdfengines_stamp.feature
│ ├── pdfengines_watermark.feature
│ ├── prometheus_metrics.feature
│ ├── root.feature
│ ├── version.feature
│ └── webhook.feature
├── main_test.go
├── scenario/
│ ├── compare.go
│ ├── containers.go
│ ├── doc.go
│ ├── http.go
│ ├── scenario.go
│ └── server.go
├── testdata/
│ ├── Special_Chars_ß.docx
│ ├── embed_1.xml
│ ├── embed_2.xml
│ ├── feature-rich-html/
│ │ └── index.html
│ ├── feature-rich-html-remote/
│ │ └── index.html
│ ├── feature-rich-markdown/
│ │ ├── index.html
│ │ └── table.md
│ ├── header-footer-html/
│ │ ├── footer.html
│ │ └── header.html
│ ├── page-1-html/
│ │ └── index.html
│ ├── page-1-markdown/
│ │ ├── index.html
│ │ └── page_1.md
│ ├── page_1.docx
│ ├── page_2.docx
│ ├── pages-12-html/
│ │ └── index.html
│ ├── pages-12-markdown/
│ │ ├── index.html
│ │ ├── page_1.md
│ │ ├── page_10.md
│ │ ├── page_11.md
│ │ ├── page_12.md
│ │ ├── page_2.md
│ │ ├── page_3.md
│ │ ├── page_4.md
│ │ ├── page_5.md
│ │ ├── page_6.md
│ │ ├── page_7.md
│ │ ├── page_8.md
│ │ └── page_9.md
│ ├── pages-3-html/
│ │ └── index.html
│ ├── pages-3-markdown/
│ │ ├── index.html
│ │ ├── page_1.md
│ │ ├── page_2.md
│ │ └── page_3.md
│ ├── pages_12.docx
│ ├── pages_3.docx
│ ├── pem/
│ │ ├── README.md
│ │ ├── cert.pem
│ │ └── key.pem
│ └── protected_page_1.docx
└── teststore/
└── .gitkeep
================================================
FILE CONTENTS
================================================
================================================
FILE: .bruno/Chromium/Convert/HTML to PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/Chromium/Convert/Markdown to PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/Chromium/Convert/URL to PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/Chromium/Screenshot/HTML Screenshot.bru
================================================
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"}
}
================================================
FILE: .bruno/Chromium/Screenshot/Markdown Screenshot.bru
================================================
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"}
}
================================================
FILE: .bruno/Chromium/Screenshot/URL Screenshot.bru
================================================
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"}
}
================================================
FILE: .bruno/Health & Info/Debug.bru
================================================
meta {
name: Debug
type: http
seq: 3
}
get {
url: {{baseUrl}}/debug
body: none
auth: none
}
================================================
FILE: .bruno/Health & Info/Health.bru
================================================
meta {
name: Health
type: http
seq: 1
}
get {
url: {{baseUrl}}/health
body: none
auth: none
}
================================================
FILE: .bruno/Health & Info/Prometheus Metrics.bru
================================================
meta {
name: Prometheus Metrics
type: http
seq: 4
}
get {
url: {{baseUrl}}/prometheus/metrics
body: none
auth: none
}
================================================
FILE: .bruno/Health & Info/Version.bru
================================================
meta {
name: Version
type: http
seq: 2
}
get {
url: {{baseUrl}}/version
body: none
auth: none
}
================================================
FILE: .bruno/LibreOffice/Convert to PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Bookmarks/Read Bookmarks.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Bookmarks/Write Bookmarks.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Convert/Convert PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Embed/Embed Files.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Encrypt/Encrypt PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Flatten/Flatten PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Merge/Merge PDFs.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Metadata/Read Metadata.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Metadata/Write Metadata.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Rotate/Rotate PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Split/Split PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Stamp/Stamp PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/PDF Engines/Watermark/Watermark PDF.bru
================================================
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"}
}
================================================
FILE: .bruno/bruno.json
================================================
{
"version": "1",
"name": "Gotenberg",
"type": "collection",
"ignore": ["node_modules", ".git"]
}
================================================
FILE: .bruno/collection.bru
================================================
headers {
Gotenberg-Trace: {{traceId}}
}
================================================
FILE: .bruno/environments/Demo.bru
================================================
vars {
baseUrl: https://demo.gotenberg.dev
traceId: bruno-demo
}
================================================
FILE: .bruno/environments/Local.bru
================================================
vars {
baseUrl: http://localhost:3000
traceId: bruno-local
}
================================================
FILE: .dockerignore
================================================
.git
================================================
FILE: .github/FUNDING.yml
================================================
github: [gulien]
================================================
FILE: .github/actions/build-test-push/action.yml
================================================
name: Build Test Push
description: Build, test and push Docker images for a given platform
author: Julien Neuhart
inputs:
github_token:
description: The GitHub token
required: true
default: ${{ github.token }}
docker_hub_username:
description: The Docker Hub username
required: true
docker_hub_password:
description: The Docker Hub password
required: true
platform:
description: linux/amd64, linux/ppc64le, linux/386, linux/arm64, linux/arm/v7
required: true
version:
description: Gotenberg version
required: true
skip_integrations_tests:
description: Define whether to skip integration testing
default: false
alternate_repository:
description: Alternate repository to push the tags to
dry_run:
description: Dry run this action
outputs:
tags:
description: Comma separated list of tag
value: ${{ steps.build.outputs.tags }}
tags_cloud_run:
description: Comma separated list of Cloud Run tags (linux/amd64 only)
value: ${{ steps.build.outputs.tags_cloud_run }}
tags_aws_lambda:
description: Comma separated list of AWS Lambda tags (linux/amd64 and linux/arm64 only)
value: ${{ steps.build.outputs.tags_aws_lambda }}
runs:
using: composite
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Check out code
uses: actions/checkout@v5
- name: Log in to Docker Hub
if: inputs.docker_hub_username != ''
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_hub_username }}
password: ${{ inputs.docker_hub_password }}
- 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 "$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 "$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 "$INPUT_TAGS" \
--dry-run "$INPUT_DRY_RUN"
- name: Outputs
shell: bash
run: |
echo "tags=${{ steps.build.outputs.tags }}"
echo "tags_cloud_run=${{ steps.build.outputs.tags_cloud_run }}"
echo "tags_aws_lambda=${{ steps.build.outputs.tags_aws_lambda }}"
================================================
FILE: .github/actions/build-test-push/build.sh
================================================
#!/bin/bash
# Exit early.
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
set -e
# Source dot env file.
source .env
# Arguments.
version=""
platform=""
alternate_repository=""
dry_run=""
while [[ $# -gt 0 ]]; do
case $1 in
--version)
version="${2//v/}"
shift 2
;;
--platform)
platform="$2"
shift 2
;;
--alternate-repository)
alternate_repository="$2"
shift 2
;;
--dry-run)
dry_run="$2"
shift 2
;;
*)
echo "Unknown option $1"
exit 1
;;
esac
done
echo "Build and push 👷"
echo
echo "Gotenberg version: $version"
echo "Target platform: $platform"
if [ -n "$alternate_repository" ]; then
DOCKER_REPOSITORY=$alternate_repository
echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY"
fi
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run"
fi
# Build tags arrays.
tags=()
tags_cloud_run=()
tags_aws_lambda=()
IFS='/' read -ra arch <<< "$platform"
IFS='.' read -ra semver <<< "$version"
if [ "${#semver[@]}" -eq 3 ]; then
echo
echo "Semver version detected"
major="${semver[0]}"
minor="${semver[1]}"
patch="${semver[2]}"
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-${arch[1]}")
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-${arch[1]}")
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-${arch[1]}")
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-${arch[1]}")
if [ "$platform" = "linux/amd64" ]; then
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-cloudrun")
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-cloudrun")
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-cloudrun")
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-cloudrun")
fi
if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:latest-aws-lambda-${arch[1]}")
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major-aws-lambda-${arch[1]}")
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor-aws-lambda-${arch[1]}")
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$major.$minor.$patch-aws-lambda-${arch[1]}")
fi
else
echo
echo "Non-semver version detected, fallback to $version"
tags+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-${arch[1]}")
if [ "$platform" = "linux/amd64" ]; then
tags_cloud_run+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-cloudrun")
fi
if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then
tags_aws_lambda+=("$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version-aws-lambda-${arch[1]}")
fi
fi
tags_flags=()
tags_cloud_run_flags=()
tags_aws_lambda_flags=()
echo "Will use the following tags:"
for tag in "${tags[@]}"; do
tags_flags+=("-t" "$tag")
echo "- $tag"
done
for tag in "${tags_cloud_run[@]}"; do
tags_cloud_run_flags+=("-t" "$tag")
echo "- $tag"
done
for tag in "${tags_aws_lambda[@]}"; do
tags_aws_lambda_flags+=("-t" "$tag")
echo "- $tag"
done
echo
# Build images.
run_cmd() {
local cmd="$1"
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run - would run the following command:"
echo "$cmd"
echo
else
echo "⚙️ Running command:"
echo "$cmd"
eval "$cmd"
echo
fi
}
join() {
local delimiter="$1"
shift
local IFS="$delimiter"
echo "$*"
}
no_arch_tag="$DOCKER_REGISTRY/$DOCKER_REPOSITORY:$version"
cmd="docker buildx build \
--build-arg GOTENBERG_VERSION=$version \
--platform $platform \
--load \
${tags_flags[*]} \
-t $no_arch_tag \
-f $DOCKERFILE $DOCKER_BUILD_CONTEXT
"
run_cmd "$cmd"
if [ "$platform" = "linux/amd64" ]; then
cmd="docker build \
--build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \
--build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \
--build-arg GOTENBERG_VERSION=$version \
${tags_cloud_run_flags[*]} \
-f $DOCKERFILE_CLOUDRUN $DOCKER_BUILD_CONTEXT
"
run_cmd "$cmd"
fi
if [ "$platform" = "linux/amd64" ] || [ "$platform" = "linux/arm64" ]; then
cmd="docker build \
--build-arg DOCKER_REGISTRY=$DOCKER_REGISTRY \
--build-arg DOCKER_REPOSITORY=$DOCKER_REPOSITORY \
--build-arg GOTENBERG_VERSION=$version \
${tags_aws_lambda_flags[*]} \
-f $DOCKERFILE_AWS_LAMBDA $DOCKER_BUILD_CONTEXT
"
run_cmd "$cmd"
fi
echo "✅ Done!"
echo "tags=$(join "," "${tags[@]}")" >> "$GITHUB_OUTPUT"
echo "tags_cloud_run=$(join "," "${tags_cloud_run[@]}")" >> "$GITHUB_OUTPUT"
echo "tags_aws_lambda=$(join "," "${tags_aws_lambda[@]}")" >> "$GITHUB_OUTPUT"
exit 0
================================================
FILE: .github/actions/build-test-push/push.sh
================================================
#!/bin/bash
# Exit early.
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
set -e
# Source dot env file.
source .env
# Arguments.
tags=""
dry_run=""
while [[ $# -gt 0 ]]; do
case $1 in
--tags)
tags="$2"
shift 2
;;
--dry-run)
dry_run=$2
shift 2
;;
*)
echo "Unknown option $1"
exit 1
;;
esac
done
echo "Push tag(s) 📦"
echo
echo "Tag(s) to push:"
IFS=',' read -ra tmp_tags_to_push <<< "$tags"
tags_to_push=()
for tag in "${tmp_tags_to_push[@]}"; do
[ -n "$tag" ] && tags_to_push+=("$tag")
done
for tag in "${tags_to_push[@]}"; do
echo "- $tag"
done
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run"
fi
echo
# Push tags.
run_cmd() {
local cmd="$1"
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run - would run the following command:"
echo "$cmd"
echo
else
echo "⚙️ Running command:"
echo "$cmd"
eval "$cmd"
echo
fi
}
for tag in "${tags_to_push[@]}"; do
cmd="docker push $tag"
run_cmd "$cmd"
echo "➡️ $tag pushed"
echo
done
echo "✅ Done!"
exit 0
================================================
FILE: .github/actions/build-test-push/test.sh
================================================
#!/bin/bash
# Exit early.
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
set -e
# Source dot env file.
source .env
# Arguments.
version=""
platform=""
alternate_repository=""
dry_run=""
while [[ $# -gt 0 ]]; do
case $1 in
--version)
version="${2//v/}"
shift 2
;;
--platform)
platform="$2"
shift 2
;;
--alternate-repository)
alternate_repository="$2"
shift 2
;;
--dry-run)
dry_run="$2"
shift 2
;;
*)
echo "Unknown option $1"
exit 1
;;
esac
done
echo "Integration testing 🧪"
echo
echo "Gotenberg version: $version"
echo "Target platform: $platform"
repository=$DOCKER_REPOSITORY
if [ -n "$alternate_repository" ]; then
echo "⚠️ Using $alternate_repository for DOCKER_REPOSITORY"
repository=$alternate_repository
fi
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run"
fi
echo
# Test image.
run_cmd() {
local cmd="$1"
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run - would run the following command:"
echo "$cmd"
echo
else
echo "⚙️ Running command:"
echo "$cmd"
eval "$cmd"
echo
fi
}
cmd="make test-integration DOCKER_REPOSITORY=$repository GOTENBERG_VERSION=$version PLATFORM=$platform NO_CONCURRENCY=true"
run_cmd "$cmd"
echo "✅ Done!"
exit 0
================================================
FILE: .github/actions/clean/action.yml
================================================
name: Clean
description: Clean tags from Docker Hub
author: Julien Neuhart
inputs:
docker_hub_username:
description: The Docker Hub username
required: true
docker_hub_password:
description: The Docker Hub password
required: true
tags:
description: Comma separated list of tags to clean
snapshot_version:
description: Snapshot version to clean
dry_run:
description: Dry run this action
runs:
using: composite
steps:
- name: Clean tags from Docker Hub
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 "$INPUT_TAGS" \
--snapshot-version "$INPUT_SNAPSHOT_VERSION" \
--dry-run "$INPUT_DRY_RUN"
================================================
FILE: .github/actions/clean/clean.sh
================================================
#!/bin/bash
# Exit early.
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
set -e
# Source dot env file.
source .env
# Arguments.
tags=""
snapshot_version=""
dry_run=""
while [[ $# -gt 0 ]]; do
case $1 in
--tags)
tags="$2"
shift 2
;;
--snapshot-version)
snapshot_version="${2//v/}"
shift 2
;;
--dry-run)
dry_run="$2"
shift 2
;;
*)
echo "Unknown option $1"
exit 1
;;
esac
done
echo "Clean tag(s) from Docker Hub 🧹"
echo
IFS=',' read -ra tags_to_delete <<< "$tags"
if [ -n "$snapshot_version" ]; then
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version")
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version-cloudrun")
tags_to_delete+=("$DOCKER_REGISTRY/snapshot:$snapshot_version-aws-lambda")
fi
echo "Will delete the following tag(s):"
for tag in "${tags_to_delete[@]}"; do
echo "- $tag"
done
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run"
fi
echo
# Delete tags.
base_url="https://hub.docker.com/v2"
token=""
if [ "$dry_run" = "true" ]; then
token="placeholder"
echo "🚧 Dry run - would call $base_url to get a token"
echo
else
echo "🌐 Get token from $base_url"
readarray -t lines < <(
curl -s -X POST \
-H "Content-Type: application/json" \
-d "{\"username\":\"$DOCKERHUB_USERNAME\", \"password\":\"$DOCKERHUB_TOKEN\"}" \
-w "\n%{http_code}" \
"$base_url/users/login"
)
http_code="${lines[-1]}"
unset 'lines[-1]'
json_body=$(printf "%s\n" "${lines[@]}")
if [ "$http_code" -ne "200" ]; then
echo "❌ Wrong HTTP status - $http_code"
echo "$json_body"
exit 1
fi
token=$(jq -r '.token' <<< "$json_body")
echo
fi
if [ -z "$token" ]; then
echo "❌ No token from Docker Hub"
exit 1
fi
for tag in "${tags_to_delete[@]}"; do
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run - would call $base_url to delete tag $tag"
echo
else
echo "🌐 Delete tag $tag"
IFS=':' read -ra tag_parts <<< "$tag"
readarray -t lines < <(
curl -s -X DELETE \
-H "Authorization: Bearer $token" \
-w "\n%{http_code}" \
"$base_url/repositories/${tag_parts[0]}/tags/${tag_parts[1]}/"
)
http_code="${lines[-1]}"
unset 'lines[-1]'
if [ "$http_code" -ne "200" ] && [ "$http_code" -ne "204" ]; then
echo "❌ Wrong HTTP status - $http_code"
printf '%s\n' "${lines[@]}"
exit 1
fi
echo
fi
done
echo "✅ Done!"
exit 0
================================================
FILE: .github/actions/merge/action.yml
================================================
name: Merge
description: Merge tags to single multi-platform tags
author: Julien Neuhart
inputs:
github_token:
description: The GitHub token
required: true
default: ${{ github.token }}
docker_hub_username:
description: The Docker Hub username
required: true
docker_hub_password:
description: The Docker Hub password
required: true
tags:
description: Comma separated tags to merge
required: true
alternate_registry:
description: Alternate registry to also push resulting tags
dry_run:
description: Dry run this action
runs:
using: composite
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Check out code
uses: actions/checkout@v5
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_hub_username }}
password: ${{ inputs.docker_hub_password }}
- 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 "$INPUT_TAGS" \
--alternate-registry "$INPUT_ALTERNATE_REGISTRY" \
--dry-run "$INPUT_DRY_RUN"
================================================
FILE: .github/actions/merge/merge.sh
================================================
#!/bin/bash
# Exit early.
# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.
set -e
# Source dot env file.
source .env
# Arguments.
tags=""
alternate_registry=""
dry_run=""
while [[ $# -gt 0 ]]; do
case $1 in
--tags)
tags="$2"
shift 2
;;
--alternate-registry)
alternate_registry="$2"
shift 2
;;
--dry-run)
dry_run=$2
shift 2
;;
*)
echo "Unknown option $1"
exit 1
;;
esac
done
echo "Merge tag(s) 👷"
echo
echo "Tag(s) to merge:"
IFS=',' read -ra tags_to_merge <<< "$tags"
for tag in "${tags_to_merge[@]}"; do
echo "- $tag"
done
if [ -n "$alternate_registry" ]; then
echo "⚠️ Will also push to $alternate_registry registry"
fi
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run"
fi
echo
# Build merge map.
declare -A merge_map
for tag in "${tags_to_merge[@]}"; do
target_tag="${tag//-amd64/}"
target_tag="${target_tag//-ppc64le/}"
target_tag="${target_tag//-arm64/}"
target_tag="${target_tag//-arm/}"
target_tag="${target_tag//-386/}"
merge_map["$target_tag"]+="$tag "
done
# Merge tags.
run_cmd() {
local cmd="$1"
if [ "$dry_run" = "true" ]; then
echo "🚧 Dry run - would run the following command:"
echo "$cmd"
echo
else
echo "⚙️ Running command:"
echo "$cmd"
eval "$cmd"
echo
fi
}
for target in "${!merge_map[@]}"; do
IFS=' ' read -ra source_tags <<< "${merge_map[$target]}"
cmd="docker buildx imagetools create \
-t $target \
${source_tags[*]}
"
run_cmd "$cmd"
echo "➡️ $target pushed"
echo
if [ -n "$alternate_registry" ]; then
alternate_target="${target/$DOCKER_REGISTRY/$alternate_registry}"
cmd="docker buildx imagetools create \
-t $alternate_target \
$target
"
run_cmd "$cmd"
echo "➡️ $alternate_target pushed"
echo
fi
done
echo "✅ Done!"
exit 0
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/build"
schedule:
interval: "weekly"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
================================================
FILE: .github/stale.yml
================================================
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 7
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 3
# Issues with these labels will never be considered stale
exemptLabels:
- bug
- documentation
- enhancement
# Label to use when marking an issue as stale
staleLabel: wontfix
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
================================================
FILE: .github/workflows/continuous-delivery.yml
================================================
name: Continuous Delivery
on:
release:
types: [published]
permissions:
contents: read
jobs:
release_amd64:
name: Release linux/amd64
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build and push
id: build_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: ${{ github.event.release.tag_name }}
platform: linux/amd64
skip_integrations_tests: true
release_386:
name: Release linux/386
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build and push
id: build_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: ${{ github.event.release.tag_name }}
platform: linux/386
skip_integrations_tests: true
release_ppc64le:
name: Release linux/ppc64le
runs-on: ubuntu-24.04-ppc64le
outputs:
tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build and push
id: build_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: ${{ github.event.release.tag_name }}
platform: linux/ppc64le
skip_integrations_tests: true
release_arm64:
name: Release linux/arm64
runs-on: ubuntu-24.04-arm
outputs:
tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build and push
id: build_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: ${{ github.event.release.tag_name }}
platform: linux/arm64
skip_integrations_tests: true
release_arm_v7:
name: Release linux/arm/v7
runs-on: ubuntu-24.04-arm
outputs:
tags: ${{ steps.build_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build and push
id: build_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: ${{ github.event.release.tag_name }}
platform: linux/arm/v7
skip_integrations_tests: true
merge_clean_release_tags:
needs:
- release_amd64
- release_386
- release_ppc64le
- release_arm64
- release_arm_v7
name: Merge and clean release tags
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Merge
uses: ./.github/actions/merge
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_ppc64le.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }}"
alternate_registry: thecodingmachine
- name: Merge AWS Lambda
uses: ./.github/actions/merge
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.release_amd64.outputs.tags_aws_lambda }},${{ needs.release_arm64.outputs.tags_aws_lambda }}"
alternate_registry: thecodingmachine
- name: Clean
uses: ./.github/actions/clean
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.release_amd64.outputs.tags }},${{ needs.release_386.outputs.tags }},${{ needs.release_ppc64le.outputs.tags }},${{ needs.release_arm64.outputs.tags }},${{ needs.release_arm_v7.outputs.tags }},${{ needs.release_amd64.outputs.tags_aws_lambda }},${{ needs.release_arm64.outputs.tags_aws_lambda }}"
================================================
FILE: .github/workflows/continuous-integration.yml
================================================
name: Continuous Integration
on:
push:
branches:
- main
pull_request:
branches:
- main
concurrency:
group: ${{ (github.event_name == 'pull_request' && github.event.pull_request.number) || 'main' }}
cancel-in-progress: true
permissions:
contents: write
jobs:
lint:
name: Lint Golang codebase
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run linters
uses: golangci/golangci-lint-action@v9
with:
version: v2.10.1
lint-prettier:
name: Lint non-Golang codebase
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .node-version
- name: Install Dependencies
run: npm i
- name: Run linters
run: make lint-prettier
test-unit:
needs:
- lint
- lint-prettier
name: Run unit tests
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: Run tests
run: make test-unit
snapshot_amd64:
if: github.event_name == 'pull_request'
needs:
- test-unit
name: Snapshot linux/amd64
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: pr-${{ github.event.pull_request.number }}
platform: linux/amd64
alternate_repository: snapshot
snapshot_ppc64le:
if: github.event_name == 'pull_request'
needs:
- test-unit
name: Snapshot linux/ppc64le
runs-on: ubuntu-24.04-ppc64le
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: pr-${{ github.event.pull_request.number }}
platform: linux/ppc64le
alternate_repository: snapshot
snapshot_386:
if: github.event_name == 'pull_request'
needs:
- test-unit
name: Snapshot linux/386
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: pr-${{ github.event.pull_request.number }}
platform: linux/386
alternate_repository: snapshot
snapshot_arm64:
if: github.event_name == 'pull_request'
needs:
- test-unit
name: Snapshot linux/arm64
runs-on: ubuntu-24.04-arm
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: pr-${{ github.event.pull_request.number }}
platform: linux/arm64
alternate_repository: snapshot
snapshot_arm_v7:
if: github.event_name == 'pull_request'
needs:
- test-unit
name: Snapshot linux/arm/v7
runs-on: ubuntu-24.04-arm
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: pr-${{ github.event.pull_request.number }}
platform: linux/arm/v7
alternate_repository: snapshot
merge_clean_snapshot_guard:
needs:
- snapshot_amd64
- snapshot_386
- snapshot_ppc64le
- snapshot_arm64
- snapshot_arm_v7
name: Secrets access check
runs-on: ubuntu-latest
outputs:
continue: ${{ steps.check.outputs.continue }}
steps:
- name: Check
id: check
run: |
if [ -n "${{ secrets.DOCKERHUB_USERNAME }}" ]; then
echo "continue=true" >> "$GITHUB_OUTPUT"
else
echo "continue=false" >> "$GITHUB_OUTPUT"
fi
merge_clean_snapshot_tags:
if: needs.merge_clean_snapshot_guard.outputs.continue == 'true'
needs:
- merge_clean_snapshot_guard
- snapshot_amd64
- snapshot_386
- snapshot_ppc64le
- snapshot_arm64
- snapshot_arm_v7
name: Merge and clean snapshot tags
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Merge
uses: ./.github/actions/merge
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_ppc64le.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }}"
- name: Merge AWS Lambda
uses: ./.github/actions/merge
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.snapshot_amd64.outputs.tags_aws_lambda }},${{ needs.snapshot_arm64.outputs.tags_aws_lambda }}"
- name: Clean
uses: ./.github/actions/clean
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.snapshot_amd64.outputs.tags }},${{ needs.snapshot_386.outputs.tags }},${{ needs.snapshot_ppc64le.outputs.tags }},${{ needs.snapshot_arm64.outputs.tags }},${{ needs.snapshot_arm_v7.outputs.tags }},${{ needs.snapshot_amd64.outputs.tags_aws_lambda }},${{ needs.snapshot_arm64.outputs.tags_aws_lambda }}"
edge_amd64:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-unit
name: Edge linux/amd64
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: edge
platform: linux/amd64
edge_386:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-unit
name: Edge linux/386
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: edge
platform: linux/386
edge_ppc64le:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-unit
name: Edge linux/ppc64le
runs-on: ubuntu-24.04-ppc64le
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: edge
platform: linux/ppc64le
edge_arm64:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-unit
name: Edge linux/arm64
runs-on: ubuntu-24.04-arm
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: edge
platform: linux/arm64
edge_arm_v7:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-unit
name: Edge linux/arm/v7
runs-on: ubuntu-24.04-arm
outputs:
tags: ${{ steps.build_test_push.outputs.tags }}
tags_cloud_run: ${{ steps.build_test_push.outputs.tags_cloud_run }}
tags_aws_lambda: ${{ steps.build_test_push.outputs.tags_aws_lambda }}
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Build, test and push
id: build_test_push
uses: ./.github/actions/build-test-push
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
version: edge
platform: linux/arm/v7
merge_clean_edge_tags:
needs:
- edge_amd64
- edge_386
- edge_ppc64le
- edge_arm64
- edge_arm_v7
name: Merge and clean edge tags
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v6
- name: Merge
uses: ./.github/actions/merge
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_ppc64le.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }}"
alternate_registry: thecodingmachine
- name: Merge AWS Lambda
uses: ./.github/actions/merge
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.edge_amd64.outputs.tags_aws_lambda }},${{ needs.edge_arm64.outputs.tags_aws_lambda }}"
alternate_registry: thecodingmachine
- name: Clean
uses: ./.github/actions/clean
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
tags: "${{ needs.edge_amd64.outputs.tags }},${{ needs.edge_386.outputs.tags }},${{ needs.edge_ppc64le.outputs.tags }},${{ needs.edge_arm64.outputs.tags }},${{ needs.edge_arm_v7.outputs.tags }},${{ needs.edge_amd64.outputs.tags_aws_lambda }},${{ needs.edge_arm64.outputs.tags_aws_lambda }}"
================================================
FILE: .github/workflows/pull-request-cleanup.yml
================================================
name: Pull Request Cleanup
on:
pull_request:
types: [closed]
permissions:
contents: read
jobs:
cleanup:
name: Cleanup Docker images
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Cleanup
uses: ./.github/actions/clean
with:
docker_hub_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_hub_password: ${{ secrets.DOCKERHUB_TOKEN }}
snapshot_version: pr-${{ github.event.pull_request.number }}
================================================
FILE: .gitignore
================================================
.idea
.vscode
.DS_Store
/node_modules/
================================================
FILE: .golangci.yml
================================================
version: "2"
run:
issues-exit-code: 1
tests: false
linters:
default: none
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- copyloopvar
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errcheck
- errname
- exhaustive
- gosec
- govet
- importas
- ineffassign
- misspell
- prealloc
- promlinter
- staticcheck
- testableexamples
- tparallel
- unconvert
- unused
- usetesting
- wastedassign
- whitespace
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
settings:
gci:
sections:
- standard
- default
- prefix(github.com/gotenberg/gotenberg/v8)
custom-order: true
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: .node-version
================================================
24.11.0
================================================
FILE: .prettierignore
================================================
build
================================================
FILE: .prettierrc
================================================
{
"plugins": ["prettier-plugin-gherkin", "prettier-plugin-sh"],
"escapeBackslashes": true
}
================================================
FILE: AGENTS.md
================================================
# Operational Guidelines for Gotenberg
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.
## Mandatory Workflow
Every task MUST follow these five steps in order. Do not skip any step.
### Step 1 — Plan
Before writing any code, produce a plan that covers:
- **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.
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.
### Step 2 — Implement
Implement the approved plan following the coding standards and patterns described in this document. After implementation, verify the build compiles (`go build ./...`).
### Step 3 — Test
Write or update tests based on the plan's testing strategy:
- **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`.
### Step 4 — Review
Self-review the implementation against the [Review Checklist](#review-checklist). Fix any issues found before presenting the result to the user.
### Step 5 — Commit
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/`).
================================================
FILE: CLAUDE.md
================================================
# 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.
================================================
FILE: CONTRIBUTING.md
================================================
# 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/`.
================================================
FILE: GEMINI.md
================================================
# 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.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Julien Neuhart
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
include .env
.PHONY: help
help: ## Show the help
@grep -hE '^[A-Za-z0-9_ \-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
.PHONY: build
build: ## Build the Gotenberg's Docker image
docker build \
-t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
-f $(DOCKERFILE) $(DOCKER_BUILD_CONTEXT)
TZ=UTC
GOTENBERG_HIDE_BANNER=false
GOTENBERG_GRACEFUL_SHUTDOWN_DURATION=30s
GOTENBERG_BUILD_DEBUG_DATA=true
API_PORT=3000
API_PORT_FROM_ENV=
API_BIND_IP=
API_START_TIMEOUT=30s
API_TIMEOUT=30s
API_BODY_LIMIT=
API_ROOT_PATH="/"
API_TRACE_HEADER=Gotenberg-Trace
API_ENABLE_BASIC_AUTH=false
GOTENBERG_API_BASIC_AUTH_USERNAME=
GOTENBERG_API_BASIC_AUTH_PASSWORD=
API-DOWNLOAD-FROM-ALLOW-LIST=
API-DOWNLOAD-FROM-DENY-LIST=
API-DOWNLOAD-FROM-FROM-MAX-RETRY=4
API-DISABLE-DOWNLOAD-FROM=false
API_DISABLE_HEALTH_CHECK_LOGGING=false
API_ENABLE_DEBUG_ROUTE=false
CHROMIUM_RESTART_AFTER=100
CHROMIUM_MAX_QUEUE_SIZE=0
CHROMIUM_MAX_CONCURRENCY=6
CHROMIUM_AUTO_START=false
CHROMIUM_START_TIMEOUT=20s
CHROMIUM_ALLOW_INSECURE_LOCALHOST=false
CHROMIUM_IGNORE_CERTIFICATE_ERRORS=false
CHROMIUM_DISABLE_WEB_SECURITY=false
CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES=false
CHROMIUM_HOST_RESOLVER_RULES=
CHROMIUM_PROXY_SERVER=
CHROMIUM_ALLOW_LIST=
CHROMIUM_DENY_LIST=^file:(?!//\/tmp/).*
CHROMIUM_CLEAR_CACHE=false
CHROMIUM_CLEAR_COOKIES=false
CHROMIUM_DISABLE_JAVASCRIPT=false
CHROMIUM_DISABLE_ROUTES=false
LIBREOFFICE_RESTART_AFTER=10
LIBREOFFICE_MAX_QUEUE_SIZE=0
LIBREOFFICE_AUTO_START=false
LIBREOFFICE_START_TIMEOUT=20s
LIBREOFFICE_DISABLE_ROUTES=false
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_ROTATE_ENGINES=pdfcpu,pdftk
PDFENGINES_EMBED_ENGINES=pdfcpu
PROMETHEUS_NAMESPACE=gotenberg
PROMETHEUS_COLLECT_INTERVAL=1s
PROMETHEUS_DISABLE_ROUTE_LOGGING=false
PROMETHEUS_DISABLE_COLLECT=false
PROMETHEUS_METRICS_PATH=/prometheus/metrics
WEBHOOK_ENABLE_SYNC_MODE=false
WEBHOOK_ALLOW_LIST=
WEBHOOK_DENY_LIST=
WEBHOOK_ERROR_ALLOW_LIST=
WEBHOOK_ERROR_DENY_LIST=
WEBHOOK_MAX_RETRY=4
WEBHOOK_RETRY_MIN_WAIT=1s
WEBHOOK_RETRY_MAX_WAIT=30s
WEBHOOK_CLIENT_TIMEOUT=30s
WEBHOOK_DISABLE=false
.PHONY: run
run: ## Start a Gotenberg container
docker run --rm -it \
-p $(API_PORT):$(API_PORT) \
-e GOTENBERG_API_BASIC_AUTH_USERNAME=$(GOTENBERG_API_BASIC_AUTH_USERNAME) \
-e GOTENBERG_API_BASIC_AUTH_PASSWORD=$(GOTENBERG_API_BASIC_AUTH_PASSWORD) \
-e TZ=$(TZ) \
$(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \
gotenberg \
--gotenberg-hide-banner=$(GOTENBERG_HIDE_BANNER) \
--gotenberg-graceful-shutdown-duration=$(GOTENBERG_GRACEFUL_SHUTDOWN_DURATION) \
--gotenberg-build-debug-data="$(GOTENBERG_BUILD_DEBUG_DATA)" \
--api-port=$(API_PORT) \
--api-port-from-env=$(API_PORT_FROM_ENV) \
--api-bind-ip=$(API_BIND_IP) \
--api-start-timeout=$(API_START_TIMEOUT) \
--api-timeout=$(API_TIMEOUT) \
--api-body-limit="$(API_BODY_LIMIT)" \
--api-root-path=$(API_ROOT_PATH) \
--api-trace-header=$(API_TRACE_HEADER) \
--api-enable-basic-auth=$(API_ENABLE_BASIC_AUTH) \
--api-download-from-allow-list=$(API-DOWNLOAD-FROM-ALLOW-LIST) \
--api-download-from-deny-list=$(API-DOWNLOAD-FROM-DENY-LIST) \
--api-download-from-max-retry=$(API-DOWNLOAD-FROM-FROM-MAX-RETRY) \
--api-disable-download-from=$(API-DISABLE-DOWNLOAD-FROM) \
--api-disable-health-check-logging=$(API_DISABLE_HEALTH_CHECK_LOGGING) \
--api-enable-debug-route=$(API_ENABLE_DEBUG_ROUTE) \
--chromium-restart-after=$(CHROMIUM_RESTART_AFTER) \
--chromium-auto-start=$(CHROMIUM_AUTO_START) \
--chromium-max-queue-size=$(CHROMIUM_MAX_QUEUE_SIZE) \
--chromium-max-concurrency=$(CHROMIUM_MAX_CONCURRENCY) \
--chromium-start-timeout=$(CHROMIUM_START_TIMEOUT) \
--chromium-allow-insecure-localhost=$(CHROMIUM_ALLOW_INSECURE_LOCALHOST) \
--chromium-ignore-certificate-errors=$(CHROMIUM_IGNORE_CERTIFICATE_ERRORS) \
--chromium-disable-web-security=$(CHROMIUM_DISABLE_WEB_SECURITY) \
--chromium-allow-file-access-from-files=$(CHROMIUM_ALLOW_FILE_ACCESS_FROM_FILES) \
--chromium-host-resolver-rules=$(CHROMIUM_HOST_RESOLVER_RULES) \
--chromium-proxy-server=$(CHROMIUM_PROXY_SERVER) \
--chromium-allow-list="$(CHROMIUM_ALLOW_LIST)" \
--chromium-deny-list="$(CHROMIUM_DENY_LIST)" \
--chromium-clear-cache=$(CHROMIUM_CLEAR_CACHE) \
--chromium-clear-cookies=$(CHROMIUM_CLEAR_COOKIES) \
--chromium-disable-javascript=$(CHROMIUM_DISABLE_JAVASCRIPT) \
--chromium-disable-routes=$(CHROMIUM_DISABLE_ROUTES) \
--libreoffice-restart-after=$(LIBREOFFICE_RESTART_AFTER) \
--libreoffice-max-queue-size=$(LIBREOFFICE_MAX_QUEUE_SIZE) \
--libreoffice-auto-start=$(LIBREOFFICE_AUTO_START) \
--libreoffice-start-timeout=$(LIBREOFFICE_START_TIMEOUT) \
--libreoffice-disable-routes=$(LIBREOFFICE_DISABLE_ROUTES) \
--log-level=$(LOG_LEVEL) \
--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-rotate-engines=$(PDFENGINES_ROTATE_ENGINES) \
--pdfengines-embed-engines=$(PDFENGINES_EMBED_ENGINES) \
--prometheus-namespace=$(PROMETHEUS_NAMESPACE) \
--prometheus-collect-interval=$(PROMETHEUS_COLLECT_INTERVAL) \
--prometheus-disable-route-logging=$(PROMETHEUS_DISABLE_ROUTE_LOGGING) \
--prometheus-disable-collect=$(PROMETHEUS_DISABLE_COLLECT) \
--prometheus-metrics-path=$(PROMETHEUS_METRICS_PATH) \
--webhook-enable-sync-mode="$(WEBHOOK_ENABLE_SYNC_MODE)" \
--webhook-allow-list="$(WEBHOOK_ALLOW_LIST)" \
--webhook-deny-list="$(WEBHOOK_DENY_LIST)" \
--webhook-error-allow-list=$(WEBHOOK_ERROR_ALLOW_LIST) \
--webhook-error-deny-list=$(WEBHOOK_ERROR_DENY_LIST) \
--webhook-max-retry=$(WEBHOOK_MAX_RETRY) \
--webhook-retry-min-wait=$(WEBHOOK_RETRY_MIN_WAIT) \
--webhook-retry-max-wait=$(WEBHOOK_RETRY_MAX_WAIT) \
--webhook-client-timeout=$(WEBHOOK_CLIENT_TIMEOUT) \
--webhook-disable=$(WEBHOOK_DISABLE)
.PHONY: test-unit
test-unit: ## Run unit tests
go test -race ./...
PLATFORM=
NO_CONCURRENCY=false
# 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-rotate
# rotate
# pdfengines-bookmarks
# bookmarks
# prometheus-metrics
# root
# version
# webhook
# download-from
TAGS=
.PHONY: test-integration
test-integration: ## Run integration tests
go test -timeout 40m -tags=integration -v github.com/gotenberg/gotenberg/v8/test/integration -args \
--gotenberg-docker-repository=$(DOCKER_REPOSITORY) \
--gotenberg-version=$(GOTENBERG_VERSION) \
--gotenberg-container-platform=$(PLATFORM) \
--no-concurrency=$(NO_CONCURRENCY) \
--tags="$(TAGS)"
.PHONY: lint
lint: ## Lint Golang codebase
golangci-lint run
.PHONY: lint-prettier
lint-prettier: ## Lint non-Golang codebase
npx prettier --check .
.PHONY: lint-todo
lint-todo: ## Find TODOs in Golang codebase
golangci-lint run --no-config --disable-all --enable godox
.PHONY: fmt
fmt: ## Format Golang codebase and "optimize" the dependencies
go fix ./...
golangci-lint fmt
go mod tidy
.PHONY: prettify
prettify: ## Format non-Golang codebase
npx prettier --write .
# go install golang.org/x/tools/cmd/godoc@latest
.PHONY: godoc
godoc: ## Run a webserver with Gotenberg godoc
$(info http://localhost:6060/pkg/github.com/gotenberg/gotenberg/v8)
godoc -http=:6060
================================================
FILE: README.md
================================================
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a69d066c4e5.png" alt="Gotenberg Logo" width="150" height="150" />
<h3 align="center">Gotenberg</h3>
<p align="center">A containerized API for seamless PDF conversion</p>
<p align="center">
<a href="https://hub.docker.com/r/gotenberg/gotenberg"><img alt="Total downloads (gotenberg/gotenberg)" src="https://img.shields.io/docker/pulls/gotenberg/gotenberg"></a>
<a href="https://github.com/gotenberg/gotenberg/actions/workflows/continuous-integration.yml"><img alt="Continuous Integration" src="https://github.com/gotenberg/gotenberg/actions/workflows/continuous-integration.yml/badge.svg"></a>
<a href="https://pkg.go.dev/github.com/gotenberg/gotenberg/v8"><img alt="Go Reference" src="https://pkg.go.dev/badge/github.com/gotenberg/gotenberg.svg"></a>
</p>
<p align="center">
<a href="https://trendshift.io/repositories/2996"><img src="https://trendshift.io/api/badge/repositories/2996" alt="gotenberg%2Fgotenberg | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</p>
<p align="center"><a href="https://gotenberg.dev/docs/getting-started/introduction">Read the Documentation</a> · <a href="https://gotenberg.dev/docs/getting-started/installation#live-demo-">Try the Live Demo</a> 🔥</p>
</p>
---
**Gotenberg** is a containerized API that abstracts the complexity of PDF conversion.
It provides a `multipart/form-data` interface for interacting with powerful engines like Chromium and LibreOffice.
Instead of managing heavy dependencies, browser versions, or fonts in your own backend, simply send your files to
Gotenberg and get a PDF in return.
## Quick Start
Open a terminal and run the following command:
```bash
docker run --rm -p 3000:3000 gotenberg/gotenberg:8
```
With the API running at `http://localhost:3000`, you are now ready to head
to the **[Full Documentation](https://gotenberg.dev/docs/getting-started/introduction)** to discover how to convert URLs,
local files, inject custom CSS, merge PDFs, and more.
## Sponsors
Open-source development takes a significant amount of time, energy, and dedication. If Gotenberg helps streamline your
workflow or powers your business, please consider supporting its continuous improvement by [**becoming a sponsor**](https://github.com/sponsors/gulien)! ❤️
**Historic & GitHub Sponsors**
- [TheCodingMachine](https://thecodingmachine.com/)
- [pdfme](https://pdfme.com/)
**Powered By**
- [Docker](https://docs.docker.com/docker-hub/repos/manage/trusted-content/dsos-program/)
- [JetBrains](https://www.jetbrains.com/community/opensource/)
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
Please ensure to keep your environment up to date and use only the latest version of Gotenberg.
Security updates and patches will be applied only to the most recent version.
## Reporting a Vulnerability
Your help in identifying vulnerabilities in our project is much appreciated.
We take all reports regarding security seriously.
If you discover a security vulnerability, please refrain from publishing it publicly.
Instead, kindly send us the details via email to _neuhart [dot] julien [at] gmail [dot] com_.
In the subject of your email, please indicate that it's a security vulnerability report for Gotenberg.
In your message, please include:
- A detailed description of the vulnerability.
- The steps to reproduce the issue.
- Any potential impact of the vulnerability on the users or system.
Please remember that this process is done in a _'best-effort'_ manner.
This means we strive to respond and act as quickly as possible, but the speed may vary depending on the severity of
the issue and our resources.
Thank you in advance for helping to keep our project safe!
## Disclosure Policy
Once we have received your vulnerability report, we will work to validate and reproduce the issue.
If we can confirm the vulnerability, we will proceed to:
- Work on a fix and a release timeline.
- Notify you when the fix has been implemented and released.
- Credit you for discovering the vulnerability (unless you request anonymity).
- Please note that we will do our best to keep you informed about the progress towards resolving the issue.
## Comments on this Policy
If you have suggestions on how this process could be improved, please submit a pull request.
================================================
FILE: build/Dockerfile
================================================
# ARG instructions do not create additional layers. Instead, next layers will
# concatenate them. Also, we have to repeat ARG instructions in each build
# stage that uses them.
ARG GOLANG_VERSION=1.26.0
# ----------------------------------------------
# pdfcpu binary build stage
# ----------------------------------------------
# Note: this stage is required as pdfcpu does not release an armhf variant by
# default.
FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage
# See https://github.com/pdfcpu/pdfcpu/releases.
ARG PDFCPU_VERSION=v0.11.1
ENV CGO_ENABLED=0
# Define the working directory outside of $GOPATH (we're using go modules).
WORKDIR /home
RUN curl -Ls "https://github.com/pdfcpu/pdfcpu/archive/refs/tags/$PDFCPU_VERSION.tar.gz" -o pdfcpu.tar.gz &&\
tar --strip-components=1 -xvzf pdfcpu.tar.gz
# Install module dependencies.
RUN go mod download &&\
go mod verify
RUN go build -o pdfcpu -ldflags "-s -w -X 'main.version=$PDFCPU_VERSION' -X 'github.com/pdfcpu/pdfcpu/pkg/pdfcpu.VersionStr=$PDFCPU_VERSION' -X main.builtBy=gotenberg" ./cmd/pdfcpu &&\
# Verify installation.
./pdfcpu version
# ----------------------------------------------
# Gotenberg binary build stage
# ----------------------------------------------
FROM golang:$GOLANG_VERSION AS gotenberg-binary-stage
ARG GOTENBERG_VERSION=snapshot
ENV CGO_ENABLED=0
# Define the working directory outside of $GOPATH (we're using go modules).
WORKDIR /home
# Install module dependencies.
COPY go.mod go.sum ./
RUN go mod download &&\
go mod verify
# Copy the source code.
COPY cmd ./cmd
COPY pkg ./pkg
RUN go build -o gotenberg -ldflags "-s -w -X 'github.com/gotenberg/gotenberg/v8/cmd.Version=$GOTENBERG_VERSION'" cmd/gotenberg/main.go
# ----------------------------------------------
# Custom JRE stage
# Credits: https://github.com/jodconverter/docker-image-jodconverter-runtime
# ----------------------------------------------
FROM debian:13-slim AS custom-jre-stage
RUN \
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends default-jdk-headless binutils
# Note: jdeps helps finding which modules a JAR requires.
# Currently only for PDFtk, as we don't rely on LibreOffice UNO Java SDK.
ENV JAVA_MODULES=java.base,java.desktop,java.naming,java.sql
RUN jlink \
--add-modules $JAVA_MODULES \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /custom-jre
# ----------------------------------------------
# Base image stage
# ----------------------------------------------
FROM debian:13-slim AS base-image-stage
ARG TIMEZONE=UTC
ENV TZ=$TIMEZONE
COPY --from=custom-jre-stage /custom-jre /opt/java
ENV PATH="/opt/java/bin:${PATH}"
# ----------------------------------------------
# Final stage
# ----------------------------------------------
FROM base-image-stage
ARG GOTENBERG_VERSION=snapshot
ARG GOTENBERG_USER_GID=1001
ARG GOTENBERG_USER_UID=1001
# See https://github.com/googlefonts/noto-emoji/releases.
ARG NOTO_COLOR_EMOJI_VERSION=v2.051
# See https://gitlab.com/pdftk-java/pdftk/-/releases - Binary package.
ARG PDFTK_VERSION=v3.3.3
LABEL org.opencontainers.image.title="Gotenberg" \
org.opencontainers.image.description="A containerized API for seamless PDF conversion." \
org.opencontainers.image.version="$GOTENBERG_VERSION" \
org.opencontainers.image.authors="Julien Neuhart <neuhart.julien@gmail.com>" \
org.opencontainers.image.documentation="https://gotenberg.dev" \
org.opencontainers.image.source="https://github.com/gotenberg/gotenberg"
RUN \
# Create a non-root user.
# All processes in the Docker container will run with this dedicated user.
groupadd --gid "$GOTENBERG_USER_GID" gotenberg &&\
useradd --uid "$GOTENBERG_USER_UID" --gid gotenberg --shell /bin/bash --home /home/gotenberg --no-create-home gotenberg &&\
mkdir /home/gotenberg &&\
chown gotenberg: /home/gotenberg
RUN \
# Install system dependencies required for the next instructions or debugging.
# Note: tini is a helper for reaping zombie processes.
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends curl gnupg tini python3 python3-distutils-extra &&\
# Cleanup.
# Note: the Debian image does automatically a clean after each install thanks to a hook.
# Therefore, there is no need for apt-get clean.
# See https://stackoverflow.com/a/24417119/3248473.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN \
# Install fonts.
# Credits:
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile.
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html.
curl -o ./ttf-mscorefonts-installer_3.8.1_all.deb http://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8.1_all.deb &&\
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
./ttf-mscorefonts-installer_3.8.1_all.deb \
culmus \
fonts-beng \
fonts-hosny-amiri \
fonts-lklug-sinhala \
fonts-lohit-guru \
fonts-lohit-knda \
fonts-samyak-gujr \
fonts-samyak-mlym \
fonts-samyak-taml \
fonts-sarai \
fonts-sil-abyssinica \
fonts-sil-padauk \
fonts-telu \
fonts-thai-tlwg \
ttf-wqy-zenhei \
fonts-arphic-ukai \
fonts-arphic-uming \
fonts-ipafont-mincho \
fonts-ipafont-gothic \
fonts-unfonts-core \
# LibreOffice recommends.
fonts-crosextra-caladea \
fonts-crosextra-carlito \
fonts-dejavu \
fonts-liberation \
fonts-liberation2 \
fonts-linuxlibertine \
fonts-noto-cjk \
fonts-noto-core \
fonts-noto-mono \
fonts-noto-ui-core \
fonts-sil-gentium \
fonts-sil-gentium-basic &&\
rm -f ./ttf-mscorefonts-installer_3.8.1_all.deb &&\
# Add Color and Black-and-White Noto emoji font.
# Credits:
# https://github.com/gotenberg/gotenberg/pull/325.
# https://github.com/googlefonts/noto-emoji.
curl -Ls "https://github.com/googlefonts/noto-emoji/raw/$NOTO_COLOR_EMOJI_VERSION/fonts/NotoColorEmoji.ttf" -o /usr/local/share/fonts/NotoColorEmoji.ttf &&\
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN \
# Install Hyphenation for LibreOffice.
# Credits: https://wiki.archlinux.org/title/LibreOffice.
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
hyphen-af hyphen-as hyphen-be hyphen-bg hyphen-bn hyphen-ca hyphen-cs hyphen-da hyphen-de hyphen-el \
hyphen-en-gb hyphen-en-us hyphen-eo hyphen-es hyphen-fr hyphen-gl hyphen-gu hyphen-hi hyphen-hr hyphen-hu \
hyphen-id hyphen-is hyphen-it hyphen-kn hyphen-lt hyphen-lv hyphen-ml hyphen-mn hyphen-mr hyphen-nl \
hyphen-no hyphen-or hyphen-pa hyphen-pl hyphen-pt-br hyphen-pt-pt hyphen-ro hyphen-ru hyphen-sk hyphen-sl \
hyphen-sr hyphen-sv hyphen-ta hyphen-te hyphen-th hyphen-uk hyphen-zu &&\
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN \
# Install Chromium.
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends chromium &&\
# Verify installation.
chromium --version &&\
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Set default characterset encoding to UTF-8.
# See:
# https://github.com/gotenberg/gotenberg/issues/104
# https://github.com/gotenberg/gotenberg/issues/730
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
RUN \
# Install LibreOffice & unoconverter. \
echo "deb http://deb.debian.org/debian trixie-backports main" >> /etc/apt/sources.list &&\
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends -t trixie-backports libreoffice &&\
curl -Ls https://raw.githubusercontent.com/gotenberg/unoconverter/v0.2.0/unoconv -o /usr/bin/unoconverter &&\
chmod +x /usr/bin/unoconverter &&\
# unoconverter will look for the Python binary, which has to be at version 3.
ln -s /usr/bin/python3 /usr/bin/python &&\
# Verify installations.
libreoffice --version &&\
unoconverter --version &&\
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN \
# Install PDFtk, QPDF & ExifTool (PDF engines).
# See https://github.com/gotenberg/gotenberg/pull/273.
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/api/v4/projects/5024297/packages/generic/pdftk-java/$PDFTK_VERSION/pdftk-all.jar" &&\
chmod a+x /usr/bin/pdftk-all.jar &&\
printf '#!/bin/bash\n\nexec java -jar /usr/bin/pdftk-all.jar "$@"' > /usr/bin/pdftk && \
chmod +x /usr/bin/pdftk &&\
apt-get update -qq &&\
apt-get upgrade -yqq &&\
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf exiftool &&\
# See https://github.com/nextcloud/docker/issues/380.
mkdir -p /usr/share/man/man1 &&\
# Verify installations.
pdftk --version &&\
qpdf --version &&\
exiftool --version &&\
# Cleanup.
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Support for arbitrary user IDs (OpenShift).
# See:
# https://github.com/gotenberg/gotenberg/issues/1049.
# https://docs.redhat.com/en/documentation/openshift_container_platform/4.15/html/images/creating-images#use-uid_create-images.
RUN \
usermod -aG root gotenberg &&\
chgrp -R 0 /home/gotenberg &&\
chmod -R g=u /home/gotenberg
# Improve fonts subpixel hinting and smoothing.
# Credits:
# https://github.com/arachnys/athenapdf/issues/69.
# https://github.com/arachnys/athenapdf/commit/ba25a8d80a25d08d58865519c4cd8756dc9a336d.
COPY build/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf
# Copy dictionnaries so that hypens work on Chromium.
# See https://github.com/gotenberg/gotenberg/issues/1293.
COPY --chown=gotenberg:gotenberg build/chromium-hyphen-data /opt/gotenberg/chromium-hyphen-data
# Copy the Golang binaries.
COPY --from=pdfcpu-binary-stage /home/pdfcpu /usr/bin/
COPY --from=gotenberg-binary-stage /home/gotenberg /usr/bin/
# Environment variables required by modules or else.
ENV CHROMIUM_BIN_PATH=/usr/bin/chromium
ENV CHROMIUM_HYPHEN_DATA_DIR_PATH=/opt/gotenberg/chromium-hyphen-data
ENV LIBREOFFICE_BIN_PATH=/usr/lib/libreoffice/program/soffice.bin
ENV UNOCONVERTER_BIN_PATH=/usr/bin/unoconverter
ENV PDFTK_BIN_PATH=/usr/bin/pdftk
ENV QPDF_BIN_PATH=/usr/bin/qpdf
ENV EXIFTOOL_BIN_PATH=/usr/bin/exiftool
ENV PDFCPU_BIN_PATH=/usr/bin/pdfcpu
USER gotenberg
WORKDIR /home/gotenberg
# Default API port.
EXPOSE 3000
ENTRYPOINT [ "/usr/bin/tini", "--" ]
CMD [ "gotenberg" ]
================================================
FILE: build/Dockerfile.aws-lambda
================================================
ARG DOCKER_REGISTRY
ARG DOCKER_REPOSITORY
ARG GOTENBERG_VERSION
FROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION
USER root
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter
# AWS.
ENV AWS_LWA_PORT=3000
ENV AWS_LWA_READINESS_CHECK_PATH=/health
ENV AWS_LWA_INVOKE_MODE=buffered
# Gotenberg.
ENV API_PORT_FROM_ENV=AWS_LWA_PORT
ENV WEBHOOK_ENABLE_SYNC_MODE=true
ENV GOTENBERG_BUILD_DEBUG_DATA=false
USER gotenberg
================================================
FILE: build/Dockerfile.cloudrun
================================================
ARG DOCKER_REGISTRY
ARG DOCKER_REPOSITORY
ARG GOTENBERG_VERSION
FROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VERSION
USER root
# For security reasons, the non-root user gotenberg does not own the Tini binary by default.
# However, some providers like Cloud Run from Google Cloud cannot start a Docker container in that case.
# See https://github.com/gotenberg/gotenberg/issues/90#issuecomment-543551353.
RUN chown gotenberg: /usr/bin/tini
# Gotenberg.
ENV API_PORT_FROM_ENV=PORT
ENV CHROMIUM_AUTO_START=true
ENV LIBREOFFICE_AUTO_START=true
ENV WEBHOOK_ENABLE_SYNC_MODE=true
ENV GOTENBERG_BUILD_DEBUG_DATA=false
ENV LOG_ENABLE_GCP_FIELDS=true
USER gotenberg
================================================
FILE: build/chromium-hyphen-data/120.0.6050.0/_metadata/verified_contents.json
================================================
[{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJoeXBoLWFmLmh5YiIsInJvb3RfaGFzaCI6ImU3S1ZpWjlhODYwT3ZfdHR1dTRDME9JODlGQUNkcjR0Z01lOGhnNU1xVUkifSx7InBhdGgiOiJoeXBoLWFzLmh5YiIsInJvb3RfaGFzaCI6InduaE9NeFdLZ0hFMWhROXhKYWZxcS1SeXM4X0hyN2dzZFBBdHBwNmlVUDQifSx7InBhdGgiOiJoeXBoLWJlLmh5YiIsInJvb3RfaGFzaCI6IlpLdnllRTdIQmlLMktnYjBwRUUzVnotRmZ4RlJoQVNQcUJHeXlCbGtkaDAifSx7InBhdGgiOiJoeXBoLWJnLmh5YiIsInJvb3RfaGFzaCI6ImRaUHdPVkNCNC02eTJGRnRFSFJtQ0tfWUpzXzlUbjQzMVRrMm1UMGdDaE0ifSx7InBhdGgiOiJoeXBoLWJuLmh5YiIsInJvb3RfaGFzaCI6InduaE9NeFdLZ0hFMWhROXhKYWZxcS1SeXM4X0hyN2dzZFBBdHBwNmlVUDQifSx7InBhdGgiOiJoeXBoLWNzLmh5YiIsInJvb3RfaGFzaCI6IklnUndJWmZEOFctRjdYbExMMHJ4TTdkYTVRc3FVQlVwS2F5SkdodlVfRXcifSx7InBhdGgiOiJoeXBoLWN1Lmh5YiIsInJvb3RfaGFzaCI6ImFiWlhPbWx5T0dnSEplVWlHMkhaQURadHA3dlM2QnI3RGh3TUF0eWV4N2sifSx7InBhdGgiOiJoeXBoLWN5Lmh5YiIsInJvb3RfaGFzaCI6Ims5Y1JTUUhCNDNiNlVNaHN6cE5nN3k2cGliTVZGOFJnQjk3MmpQVGNvYkEifSx7InBhdGgiOiJoeXBoLWRhLmh5YiIsInJvb3RfaGFzaCI6IlRMZk92MjdUTFFpSDdWaFNIbDlCblQydDlKSkl1WEpDMWlFWUxRS251bGcifSx7InBhdGgiOiJoeXBoLWRlLTE5MDEuaHliIiwicm9vdF9oYXNoIjoiMHlHekNnc2tpTGI1STJoTC0yc1FCVmJMXzNCekE4VFNwSUZ6aDltd1ZsYyJ9LHsicGF0aCI6Imh5cGgtZGUtMTk5Ni5oeWIiLCJyb290X2hhc2giOiJIMGVZZHhlbDNyZU15UHRqVEt2QUI4RWFzaEFTbGpMUmhZOU83c0ljUFVRIn0seyJwYXRoIjoiaHlwaC1kZS1jaC0xOTAxLmh5YiIsInJvb3RfaGFzaCI6InpMQVlIVGVvc3IwdlBrcTc2VjdJM083b0V1cUI5M3NtSmxqNThibjZuYWMifSx7InBhdGgiOiJoeXBoLWVsLmh5YiIsInJvb3RfaGFzaCI6IjFOazV4S1JiR1ZYVElCUkVIbjB2SFJzU1VNTjZfdDAzdTVtRkwzMEtNN3MifSx7InBhdGgiOiJoeXBoLWVuLWdiLmh5YiIsInJvb3RfaGFzaCI6IlZvR2ZOaHpnajBOQ29qelhscjBQdjFSdnpFTEZJVFJ3MURRTWRUMXZiT0kifSx7InBhdGgiOiJoeXBoLWVuLXVzLmh5YiIsInJvb3RfaGFzaCI6Il94OUFGM2dFMzBLelE0bHFRU1BqLWZXWnl0bnNqLURWQVgzdDRqZEVUVXMifSx7InBhdGgiOiJoeXBoLWVzLmh5YiIsInJvb3RfaGFzaCI6IjBmdWc0YWVadDc0Z19XbEVyNUtsY1JHWkVkMzJXZFEtWFptSkxZX2xuRWsifSx7InBhdGgiOiJoeXBoLWV0Lmh5YiIsInJvb3RfaGFzaCI6ImxkUFIwUm14R3EyZ3EzNFF1Ylp6LXRlRGtvWFFibmg4VjM2bjIyRkNxY0EifSx7InBhdGgiOiJoeXBoLWV1Lmh5YiIsInJvb3RfaGFzaCI6IjRuZUtUOGU0OEdTaksycEV2Q254RGlaTm5XSVV1TzI0NjlIMTl0YU9MckkifSx7InBhdGgiOiJoeXBoLWZyLmh5YiIsInJvb3RfaGFzaCI6IjFudGF1Nm9FVUtQbWV2SFJKSkwydEc5c1FYQmxOcHFSZFJxYlZpMnJZeDAifSx7InBhdGgiOiJoeXBoLWdhLmh5YiIsInJvb3RfaGFzaCI6ImxGLVlGb3VwcUItempfM1ZadFc0aEw4Uk51Ql9YREpna0p2N1VMMFJFc1kifSx7InBhdGgiOiJoeXBoLWdsLmh5YiIsInJvb3RfaGFzaCI6IlJBU1hfb0MxVzFDUmtOYURETC0xZVoxYnYyS0c0Y2hfWE1jUEU4cXRpY1kifSx7InBhdGgiOiJoeXBoLWd1Lmh5YiIsInJvb3RfaGFzaCI6InJ3N2JaOElobTRBOFByYkIzdWJ5MUJvXzRBUm9xZHFMNk85UVZ0Y0JxX00ifSx7InBhdGgiOiJoeXBoLWhpLmh5YiIsInJvb3RfaGFzaCI6IjlOOGlUVVdmMFJGcGpkV2hOaFBGdV9EdEVmQkNlTllDTU5Bb0FRNnNERUkifSx7InBhdGgiOiJoeXBoLWhyLmh5YiIsInJvb3RfaGFzaCI6IjFmQm1wV1ZfSFh3NTBGT1ZiZklFdDVKdlFOTC1UMmxYT3ZDZGtKQm00bXcifSx7InBhdGgiOiJoeXBoLWh1Lmh5YiIsInJvb3RfaGFzaCI6InExWmRIaTR3VElWbFFiSHhVdW5NVEJaaEMya29JWTg1d3pUTnE0aUhTVlEifSx7InBhdGgiOiJoeXBoLWh5Lmh5YiIsInJvb3RfaGFzaCI6Im16VGZ5b1hMSjFSb0tmRUU4VGQxZnZzblNUVEI2ZFNaSDFXdFZrbGlwMm8ifSx7InBhdGgiOiJoeXBoLWl0Lmh5YiIsInJvb3RfaGFzaCI6Ii1jQW4xXzFFc0J6VjRjMzRBdUlNWTFZR2N3bUs4WXZxQ1RDNm12TTA0UGMifSx7InBhdGgiOiJoeXBoLWthLmh5YiIsInJvb3RfaGFzaCI6IlZoTFVGQnBOSDg5RDU2WXVPRmx4dnRqTTBJcjZfVTRLMUJacXB6NzVmaTAifSx7InBhdGgiOiJoeXBoLWtuLmh5YiIsInJvb3RfaGFzaCI6Iks1bWRDaFV2Z0VZQnFvODRfdzA2YmxsSmwzdngycWR2cUlpc3JpRlNZb2MifSx7InBhdGgiOiJoeXBoLWxhLmh5YiIsInJvb3RfaGFzaCI6Il9VdHZOaE5jMDdreTQxRHNJQmZmMkowdU5xd2liMVRreVBMa3ZHMndXVDAifSx7InBhdGgiOiJoeXBoLWx0Lmh5YiIsInJvb3RfaGFzaCI6Il9pbnpod2o5ZEtMZ3NOeDdVOHV1TGE4WVlXZUFnZVZQb2pVVUJ2eVZPUkUifSx7InBhdGgiOiJoeXBoLWx2Lmh5YiIsInJvb3RfaGFzaCI6Imtkc0Ytd1FuNHpQQzNySW83ekw0UUZLNlJ4NkNZVjZmVkhzd3dBM0tDV2MifSx7InBhdGgiOiJoeXBoLW1sLmh5YiIsInJvb3RfaGFzaCI6ImtGY3R1UFNiQWV4cUVDY3l6ZkZQd19COU5qeS1EU1lSQS1XREJERms2SWcifSx7InBhdGgiOiJoeXBoLW1uLWN5cmwuaHliIiwicm9vdF9oYXNoIjoiMm5yb3g2UFNHU19XQ1FZWUk3SnZ0cWwxMlhjUHVTd3UxMk1aS2VMT1QzayJ9LHsicGF0aCI6Imh5cGgtbXIuaHliIiwicm9vdF9oYXNoIjoiOU44aVRVV2YwUkZwamRXaE5oUEZ1X0R0RWZCQ2VOWUNNTkFvQVE2c0RFSSJ9LHsicGF0aCI6Imh5cGgtbXVsLWV0aGkuaHliIiwicm9vdF9oYXNoIjoiOHZyQnZRYWZfbHpSRVMyVXpERVRmdE9LR3hZUWstelhUSndXaUVLTGFJcyJ9LHsicGF0aCI6Imh5cGgtbmIuaHliIiwicm9vdF9oYXNoIjoidW1oN2VNX0ptaVRpdVdjeUNSU2Y0eGVnT085aDZaczZxcl9XeHdtQk9IdyJ9LHsicGF0aCI6Imh5cGgtbmwuaHliIiwicm9vdF9oYXNoIjoiMWNMSjEtZ0J3UkhNMDlhVExINVZZOWpXeGY2cUpqYjgydFdSX0tsRlg5ZyJ9LHsicGF0aCI6Imh5cGgtbm4uaHliIiwicm9vdF9oYXNoIjoiVVRNblpKaGR0LW51UGEwSGRBMmpqeE9yUU9CMTZ4UVk3ZFo1b2dKeVB2MCJ9LHsicGF0aCI6Imh5cGgtb3IuaHliIiwicm9vdF9oYXNoIjoiVHB6VEFycl94T28tbGxJeWZxSkFjdXZ6ZTF4UHdIR1NrcjJzRUtxdFpscyJ9LHsicGF0aCI6Imh5cGgtcGEuaHliIiwicm9vdF9oYXNoIjoiUndNcDBvLXFTRS1VWFhqXzc3RjIzTGJ5QXl4MVBpVzhBVUVHclNTeXhvbyJ9LHsicGF0aCI6Imh5cGgtcHQuaHliIiwicm9vdF9oYXNoIjoiOXZ2eHZMSmd6SVlsYjhTVTg0ajNzbjBRaGwtX2oyRlJmZTRscjAxWTF1ZyJ9LHsicGF0aCI6Imh5cGgtcnUuaHliIiwicm9vdF9oYXNoIjoicXN2dk9SNU5oUWlrYV8zVXU5N3QwQ0tWU2o2RFhPSVFFMVVXbWRmR1VRdyJ9LHsicGF0aCI6Imh5cGgtc2suaHliIiwicm9vdF9oYXNoIjoiN2Z4MDBSMHQtYjVscVVlX3hGNy1pVThuNkZUTzJrVjNmYy1odGdEQVZlYyJ9LHsicGF0aCI6Imh5cGgtc2wuaHliIiwicm9vdF9oYXNoIjoiT1hDWTBsMS0wYzZ2eVk4YmpURTBObEJBSnlvUVl5YmFfOVp0WVN0UF83byJ9LHsicGF0aCI6Imh5cGgtc3EuaHliIiwicm9vdF9oYXNoIjoidkNuSlFCenBVa0ZNdXV2RnlPNGRKOEZ3Ykc5M2dIdGY5eFBpRWtRNHM4byJ9LHsicGF0aCI6Imh5cGgtc3YuaHliIiwicm9vdF9oYXNoIjoiR1hhQU9rUmRyWE5ac1FLbHBKX3lCd1doZUNpRzhjZFNzREZ4OWc3MnJwOCJ9LHsicGF0aCI6Imh5cGgtdGEuaHliIiwicm9vdF9oYXNoIjoiUVAycFNGYW9id1pkNkxxbUdFNm1QYzJ3RWU1TXBKaW53ZjdrVEpreFRHYyJ9LHsicGF0aCI6Imh5cGgtdGUuaHliIiwicm9vdF9oYXNoIjoiVVctcFpVLWpycXEwZ05RT3IyclhqOEE1Q0d2WTdjRkV2ajFaVWw3Y3JDayJ9LHsicGF0aCI6Imh5cGgtdGsuaHliIiwicm9vdF9oYXNoIjoiZF8ydTBwdllRcXFwZHF0LS1CdGhlaFhBb3RIcjBSWWNHX0pyZWFFSXRjMCJ9LHsicGF0aCI6Imh5cGgtdWsuaHliIiwicm9vdF9oYXNoIjoieWxjVXUzT05ZS3N1LW9pS3R5VWNSak1PQnhwZzBMdjdMNENvZHpsUW5zayJ9LHsicGF0aCI6Imh5cGgtdW5kLWV0aGkuaHliIiwicm9vdF9oYXNoIjoiSGVnOHQ0ZmZyMVA3Zm02TnM2cmxBSXJTVHIzU2ktQWdNVEJ1cWVuejRvVSJ9LHsicGF0aCI6Im1hbmlmZXN0Lmpzb24iLCJyb290X2hhc2giOiIxWEh2TTdEbkIxY2ZFTHMzdVpwZ2ZXOURyLU1fVTlGYlE1V3hidlA5cG1VIn1dLCJmb3JtYXQiOiJ0cmVlaGFzaCIsImhhc2hfYmxvY2tfc2l6ZSI6NDA5Nn1dLCJpdGVtX2lkIjoiamFtaGNubmtpaGlubWRsa2Fra2FvcGJqYmJjbmdmbGMiLCJpdGVtX3ZlcnNpb24iOiIxMjAuMC42MDUwLjAiLCJwcm90b2NvbF92ZXJzaW9uIjoxfQ","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"ud33uh3_3o_eTIMSj_MKboC9-GzBxQ-Bu6XS31wn7JB3ntcoVSUfAgMjTBCsIYEEgqVfKJlf92wgl3SbjJWaT-_XfV8sMFwZtuAT0qJV0p9gammnprPP0OmUwJdJB-kK1MO8ESwSyeGKCEeIXGDqAVdQHkYD-oKzYS-zKhe9KVnU-WtJ6mtG80ybhjxJDM1aLyS6_ocXKYBmcB9av0IY-saDVR7hkVNjc-iR9lhYI1682VbDmlQ9-uueCkK4YsqmO1mOSgYcQ-Hm56zQxhGrMHbGokIX667-8yHRbxjoag7eNxHrY5VQI-te17pDKE9G9cz87qvGSMPUi9QGdyt7a1652KWPXe6bDEOjIoaHUq9juOd7r8SxYCv8tqhAx5nxhqkaq9oHSfiYrrcddaSdtdCOYo7hyqVQV1562x0NZiNrGH9sU5V-e_5DAmPVqBMA1yjY3ZQEWWyTdQ_Wtw5qbs3m5qh5Grut8RtIb7yGJJsamDN3LG73jcrtXZ1cMynqN3LysksG8Y73RfO3joVhy3gw5Y1X6ES1gvQi4n1hxvOCCXoGIbIJwIZGjTlcuh2J_eweLo0hm1IeXK_lAB9P1RiruKfEc0P75CY4V_LDziEdFHxIpFS6PjH94n1aAj0F3ba6opqyjgXs6n8uuhoJvdq5GwnAuwsOzs771p8mWl0"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"fLFplPrKe8OWo-G7YhCQxsnj2MHPUqYvWL9ACSCD1WuA4K5c0pOFNMZ10w0Po0lgprE7LTCjWTk3pKKvyTxojWAyAg-c75DU2kfnntDabBEn9ooCiBcWJIuOkJMdcLYBbfe-t-JO0KPKm-2mGi59MkO9xir2MMwAqtITGdrH4WXjHTIB6guYQMtre_Bp_zqvZnGQKqZI0Cdq8QVqd7z69_j63fvv0CjXuZ-6F1RNElS75H3FzJ1OrVMCOjEOaKyk1DD-aqgr-6lUq2er1XWrf9JxtAmAawpnh3RAEi_1VoGtbga92USt_0ZLiapoC4PlWSloLuX-_NYFg9gtPJNS9w"}]}}]
================================================
FILE: build/chromium-hyphen-data/120.0.6050.0/manifest.json
================================================
{
"manifest_version": 2,
"name": "hyphens-data",
"version": "120.0.6050.0"
}
================================================
FILE: build/fonts.conf
================================================
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
<match target="font">
<edit mode="assign" name="rgba">
<const>rgb</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hinting">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hintstyle">
<const>hintslight</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="antialias">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="lcdfilter">
<const>lcddefault</const>
</edit>
</match>
</fontconfig>
================================================
FILE: cmd/gotenberg/main.go
================================================
package main
import (
gotenbergcmd "github.com/gotenberg/gotenberg/v8/cmd"
// Gotenberg modules.
_ "github.com/gotenberg/gotenberg/v8/pkg/standard"
)
func main() {
gotenbergcmd.Run()
}
================================================
FILE: cmd/gotenberg.go
================================================
package gotenbergcmd
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
flag "github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
// See https://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Gotenberg.
// Credits: https://github.com/labstack/echo/blob/v4.3.0/echo.go#L240.
const banner = `
_____ __ __
/ ___/__ / /____ ___ / / ___ _______ _
/ (_ / _ \/ __/ -_) _ \/ _ \/ -_) __/ _ '/
\___/\___/\__/\__/_//_/_.__/\__/_/ \_, /
/___/
A containerized API for seamless PDF conversion.
Version: %s
-------------------------------------------------------
`
// Version is the... version of the Gotenberg application. We set it at the
// build stage of the Docker image.
var Version = "snapshot"
// Run starts the Gotenberg application. Call this in the main of your program.
func Run() {
gotenberg.Version = Version
// Create the root FlagSet and adds the modules flags to it.
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
fs.Bool("gotenberg-hide-banner", false, "Hide the banner")
fs.Duration("gotenberg-graceful-shutdown-duration", time.Duration(30)*time.Second, "Set the graceful shutdown duration")
fs.Bool("gotenberg-build-debug-data", true, "Set if build data is needed")
descriptors := gotenberg.GetModuleDescriptors()
var modsInfo strings.Builder
for _, desc := range descriptors {
fs.AddFlagSet(desc.FlagSet)
modsInfo.WriteString(desc.ID + " ")
}
// Parse the flags.
err := fs.Parse(os.Args[1:])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// Override their values if the corresponding environment variables are
// set.
fs.VisitAll(func(f *flag.Flag) {
envName := strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
val, ok := os.LookupEnv(envName)
if !ok {
return
}
sliceVal, ok := f.Value.(flag.SliceValue)
if ok {
// We don't want to append the values (default pflag behavior).
items := strings.Split(val, ",")
err = sliceVal.Replace(items)
if err != nil {
fmt.Printf("[FATAL] invalid overriding value '%s' from %s: %v\n", val, envName, err)
os.Exit(1)
}
return
}
err = f.Value.Set(val)
if err != nil {
fmt.Printf("[FATAL] invalid overriding value '%s' from %s: %v\n", val, envName, err)
os.Exit(1)
}
})
// Create a wrapper around our flags.
parsedFlags := gotenberg.ParsedFlags{FlagSet: fs}
hideBanner := parsedFlags.MustBool("gotenberg-hide-banner")
gracefulShutdownDuration := parsedFlags.MustDuration("gotenberg-graceful-shutdown-duration")
if !hideBanner {
fmt.Printf(banner, Version)
}
fmt.Printf("[SYSTEM] modules: %s\n", modsInfo.String())
ctx := gotenberg.NewContext(parsedFlags, descriptors)
// Start application modules.
apps, err := ctx.Modules(new(gotenberg.App))
if err != nil {
fmt.Printf("[FATAL] %s\n", err)
os.Exit(1)
}
for _, a := range apps {
go func(app gotenberg.App) {
id := app.(gotenberg.Module).Descriptor().ID
err = app.Start()
if err != nil {
fmt.Printf("[FATAL] starting %s: %s\n", id, err)
os.Exit(1)
}
startupMessage := app.StartupMessage()
if startupMessage == "" {
fmt.Printf("[SYSTEM] %s: application started\n", id)
return
}
fmt.Printf("[SYSTEM] %s: %s\n", id, startupMessage)
}(a.(gotenberg.App))
}
// Get modules that want to print system messages.
sysLoggers, err := ctx.Modules(new(gotenberg.SystemLogger))
if err != nil {
fmt.Printf("[FATAL] %s\n", err)
os.Exit(1)
}
for _, l := range sysLoggers {
go func(logger gotenberg.SystemLogger) {
id := logger.(gotenberg.Module).Descriptor().ID
for _, message := range logger.SystemMessages() {
fmt.Printf("[SYSTEM] %s: %s\n", id, message)
}
}(l.(gotenberg.SystemLogger))
}
if parsedFlags.MustBool("gotenberg-build-debug-data") {
// Build the debug data.
gotenberg.BuildDebug(ctx)
}
quit := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) or SIGTERM (Kubernetes).
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Block until we receive our signal.
<-quit
gracefulShutdownCtx, cancel := context.WithTimeout(context.Background(), gracefulShutdownDuration)
defer cancel()
forceQuit := make(chan os.Signal, 1)
signal.Notify(forceQuit, syscall.SIGINT)
go func() {
// In case of force quit, cancel the context.
<-forceQuit
cancel()
}()
fmt.Printf("[SYSTEM] graceful shutdown of %s\n", gracefulShutdownDuration)
eg, _ := errgroup.WithContext(gracefulShutdownCtx)
for _, a := range apps {
eg.Go(func(app gotenberg.App) func() error {
return func() error {
id := app.(gotenberg.Module).Descriptor().ID
err = app.Stop(gracefulShutdownCtx)
if errors.Is(err, gotenberg.ErrCancelGracefulShutdownContext) {
cancel()
} else if err != nil {
return fmt.Errorf("stopping %s: %w", id, err)
}
fmt.Printf("[SYSTEM] %s: application stopped\n", id)
return nil
}
}(a.(gotenberg.App)))
}
err = eg.Wait()
if err != nil {
fmt.Printf("[FATAL] %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
================================================
FILE: go.mod
================================================
module github.com/gotenberg/gotenberg/v8
go 1.26.0
require (
github.com/alexliesenfeld/health v0.8.1
github.com/barasher/go-exiftool v1.10.0
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.2
github.com/cucumber/godog v0.15.1
github.com/dlclark/regexp2 v1.11.5
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-connections v0.6.0
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab
github.com/google/uuid v1.6.0
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/labstack/echo/v4 v4.15.1
github.com/labstack/gommon v0.4.2
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.2
github.com/spf13/pflag v1.0.10
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.52.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.41.0
golang.org/x/text v0.35.0
)
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
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
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
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.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
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.5 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
github.com/magiconair/properties v1.8.10 // indirect
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.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
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
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.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.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
github.com/stretchr/testify v1.11.1 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
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.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.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.4 // indirect
go4.org v0.0.0-20260112195520-a5071408f32f // 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
)
================================================
FILE: go.sum
================================================
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
github.com/alexliesenfeld/health v0.8.1 h1:wdE3vt+cbJotiR8DGDBZPKHDFoJbAoWEfQTcqrmedUg=
github.com/alexliesenfeld/health v0.8.1/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=
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=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI=
github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8=
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
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.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=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao=
github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc=
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo=
github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
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.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=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
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.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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
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.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.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=
github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
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.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=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
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.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.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=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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.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.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.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=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
================================================
FILE: package.json
================================================
{
"devDependencies": {
"prettier": "3.8.1",
"prettier-plugin-gherkin": "^3.1.3",
"prettier-plugin-sh": "^0.18.0"
}
}
================================================
FILE: pkg/gotenberg/cmd.go
================================================
package gotenberg
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os/exec"
"strings"
"syscall"
"go.uber.org/zap"
)
// Cmd wraps an [exec.Cmd].
type Cmd struct {
ctx context.Context
logger *zap.Logger
process *exec.Cmd
}
// Command creates a [Cmd] without a context. It configures the internal
// [exec.Cmd] of [Cmd] so that we may kill its unix process and all its
// children without creating orphans.
//
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
func Command(logger *zap.Logger, binPath string, args ...string) *Cmd {
cmd := exec.Command(binPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return &Cmd{
ctx: nil,
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
process: cmd,
}
}
// CommandContext creates a [Cmd] with a context. It configures the internal
// [exec.Cmd] of [Cmd] so that we may kill its unix process and all its
// children without creating orphans.
//
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
func CommandContext(ctx context.Context, logger *zap.Logger, binPath string, args ...string) (*Cmd, error) {
if ctx == nil {
return nil, errors.New("nil context")
}
cmd := exec.CommandContext(ctx, binPath, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
return &Cmd{
ctx: ctx,
logger: logger.Named(strings.ReplaceAll(binPath, "/", "")),
process: cmd,
}, nil
}
// Start starts the command but does not wait for its completion.
func (cmd *Cmd) Start() error {
err := cmd.pipeOutput()
if err != nil {
return fmt.Errorf("pipe unix process output: %w", err)
}
cmd.logger.Debug(fmt.Sprintf("start unix process: %s", strings.Join(cmd.process.Args, " ")))
err = cmd.process.Start()
if err != nil {
return fmt.Errorf("start unix process: %w", err)
}
return nil
}
// Wait waits for the command to complete. It should be called when using the
// Start method so that the command does not leak zombies.
func (cmd *Cmd) Wait() error {
err := cmd.process.Wait()
if err != nil {
return fmt.Errorf("wait for unix process: %w", err)
}
return nil
}
// Exec executes the command and waits for its completion or until the context
// is done. In any case, it kills the unix process and all its children.
func (cmd *Cmd) Exec() (int, error) {
if cmd.ctx == nil {
return 10, errors.New("nil context")
}
err := cmd.Start()
if err != nil {
if cmd.process.ProcessState == nil {
return 131, fmt.Errorf("start command: %w", err)
}
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("start command: %w", err)
}
errChan := make(chan error, 1)
go func() {
errChan <- cmd.Wait()
}()
select {
case err = <-errChan:
errProc := cmd.Kill()
if errProc != nil {
cmd.logger.Error(errProc.Error())
}
if err == nil {
return 0, nil
}
if cmd.process.ProcessState == nil {
return 131, fmt.Errorf("unix process error: %w", err)
}
return cmd.process.ProcessState.ExitCode(), fmt.Errorf("unix process error: %w", err)
case <-cmd.ctx.Done():
errProc := cmd.Kill()
if errProc != nil {
cmd.logger.Error(errProc.Error())
}
return 62, fmt.Errorf("context done: %w", cmd.ctx.Err())
}
}
// pipeOutput creates logs entries according to the process stdout and stderr.
// It does nothing if the logging level is not debug.
func (cmd *Cmd) pipeOutput() error {
checkedEntry := cmd.logger.Check(zap.DebugLevel, "check for debug level before piping unix process output")
if checkedEntry == nil {
return nil
}
stdout, err := cmd.process.StdoutPipe()
if err != nil {
return fmt.Errorf("pipe unix process stdout: %w", err)
}
stderr, err := cmd.process.StderrPipe()
if err != nil {
return fmt.Errorf("unix process sdterr: %w", err)
}
// logCommandOutput creates logs entries according to a reader
// (either stdout or stderr).
logCommandOutput := func(logger *zap.Logger, reader io.ReadCloser) {
r := bufio.NewReader(reader)
defer func(reader io.ReadCloser) {
err := reader.Close()
if err != nil && !strings.Contains(err.Error(), "file already closed") {
logger.Error(fmt.Sprintf("close reader: %s", err))
}
}(reader)
for {
line, _, err := r.ReadLine()
if err != nil {
if err != io.EOF && !strings.Contains(err.Error(), "file already closed") {
logger.Error(fmt.Sprintf("pipe unix process output error: %s", err))
}
break
}
if len(line) != 0 {
logger.Debug(string(line))
}
}
}
go logCommandOutput(cmd.logger.Named("stdout"), stdout)
go logCommandOutput(cmd.logger.Named("stderr"), stderr)
return nil
}
// Kill kills the unix process and all its children without creating orphans.
//
// See https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773.
func (cmd *Cmd) Kill() error {
if cmd.process == nil {
// We cannot use the logger here, because for whatever reason using it
// result to a panic.
// cmd.logger.Debug("no process, skip killing")
return nil
}
err := syscall.Kill(-cmd.process.Process.Pid, syscall.SIGKILL)
if err == nil {
cmd.logger.Debug("unix process killed")
return nil
}
// If the process does not exist anymore, the error is irrelevant.
if strings.Contains(err.Error(), "no such process") {
cmd.logger.Debug("unix process already killed")
return nil
}
return fmt.Errorf("kill unix process: %w", err)
}
================================================
FILE: pkg/gotenberg/context.go
================================================
package gotenberg
import (
"fmt"
"reflect"
)
// Context is a struct that helps to initialize modules. When provisioning, a
// module may use the context to get other modules that it needs internally.
type Context struct {
flags ParsedFlags
descriptors []ModuleDescriptor
moduleInstances map[string]any
}
// NewContext creates a [Context].
// In a module, prefer the [Provisioner] interface to get a [Context].
func NewContext(
flags ParsedFlags,
descriptors []ModuleDescriptor,
) *Context {
return &Context{
flags: flags,
descriptors: descriptors,
moduleInstances: make(map[string]any),
}
}
// ParsedFlags returns the parsed flags.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// flags := ctx.ParsedFlags()
// m.foo = flags.RequiredString("foo")
// }
func (ctx *Context) ParsedFlags() ParsedFlags {
return ctx.flags
}
// Module returns a module which satisfies the requested interface.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// mod, _ := ctx.Module(new(ModuleInterface))
// real := mod.(ModuleInterface)
// }
//
// If the module has not yet been initialized, this method
// initializes it. Otherwise, returns the already initialized instance.
func (ctx *Context) Module(kind any) (any, error) {
mods, err := ctx.Modules(kind)
if err != nil {
return nil, fmt.Errorf("get module: %w", err)
}
if len(mods) != 1 {
return nil, fmt.Errorf("expected to have one and only one %s module", kind)
}
return mods[0], nil
}
// Modules return the list of modules which satisfies the requested interface.
//
// func (m *YourModule) Provision(ctx *gotenberg.Context) error {
// mods, _ := ctx.Modules(new(ModuleInterface))
// for _, mod := range mods {
// real := mod.(ModuleInterface)
// // ...
// }
// }
//
// If one or more modules have not yet been initialized, this method
// initializes them. Otherwise, returns the already initialized instances.
func (ctx *Context) Modules(kind any) ([]any, error) {
realKind := reflect.TypeOf(kind).Elem()
var mods []any
for _, desc := range ctx.descriptors {
newInstance := desc.New()
if ok := reflect.TypeOf(newInstance).Implements(realKind); ok {
// The module implements the requested interface.
// We check if it has already been initialized.
instance, ok := ctx.moduleInstances[desc.ID]
if ok {
mods = append(mods, instance)
} else {
err := ctx.loadModule(desc.ID, newInstance)
if err != nil {
return nil, err
}
mods = append(mods, newInstance)
}
}
}
return mods, nil
}
// loadModule calls the Provision and/or Validate methods of the requested
// module if it satisfies the [Provisioner] and/or [Validator] interfaces.
func (ctx *Context) loadModule(id string, instance any) error {
if prov, ok := instance.(Provisioner); ok {
// The instance can be provisioned.
err := prov.Provision(ctx)
if err != nil {
return fmt.Errorf("provision module %s: %w", id, err)
}
}
if validator, ok := instance.(Validator); ok {
// The instance can be validated.
err := validator.Validate()
if err != nil {
return fmt.Errorf("validate module %s: %w", id, err)
}
}
ctx.moduleInstances[id] = instance
return nil
}
================================================
FILE: pkg/gotenberg/context_test.go
================================================
package gotenberg
import (
"errors"
"testing"
)
func TestContext_Module(t *testing.T) {
for _, tc := range []struct {
scenario string
mods []ModuleDescriptor
kind any
expectError bool
}{
{
scenario: "module with error on provision",
mods: func() []ModuleDescriptor {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return errors.New("foo") }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
expectError: true,
},
{
scenario: "two modules instead of one",
mods: func() []ModuleDescriptor {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
}(),
kind: new(Provisioner),
expectError: true,
},
{
scenario: "success",
mods: func() []ModuleDescriptor {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
ctx := NewContext(ParsedFlags{}, tc.mods)
_, err := ctx.Module(tc.kind)
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 TestContext_Modules(t *testing.T) {
for _, tc := range []struct {
scenario string
mods []ModuleDescriptor
kind any
expectError bool
}{
{
scenario: "module with error on provision",
mods: func() []ModuleDescriptor {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return errors.New("foo") }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
expectError: true,
},
{
scenario: "success (module)",
mods: func() []ModuleDescriptor {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor(), mod.Descriptor()}
}(),
kind: new(Provisioner),
expectError: false,
},
{
scenario: "success (one module)",
mods: func() []ModuleDescriptor {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return nil }
return []ModuleDescriptor{mod.Descriptor()}
}(),
kind: new(Provisioner),
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
ctx := NewContext(ParsedFlags{}, tc.mods)
_, err := ctx.Modules(tc.kind)
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 TestContext_loadModule(t *testing.T) {
for _, tc := range []struct {
scenario string
instance any
expectError bool
}{
{
scenario: "module with error on provision",
instance: func() any {
mod := &struct {
ModuleMock
ProvisionerMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ProvisionMock = func(ctx *Context) error { return errors.New("foo") }
return mod
}(),
expectError: true,
},
{
scenario: "module with error on validation",
instance: func() any {
mod := &struct {
ModuleMock
ValidatorMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ValidateMock = func() error { return errors.New("foo") }
return mod
}(),
expectError: true,
},
{
scenario: "success",
instance: func() any {
mod := &struct {
ModuleMock
ValidatorMock
}{}
mod.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod }}
}
mod.ValidateMock = func() error { return nil }
return mod
}(),
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
ctx := NewContext(ParsedFlags{}, nil)
err := ctx.loadModule("foo", tc.instance)
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")
}
})
}
}
================================================
FILE: pkg/gotenberg/debug.go
================================================
package gotenberg
import (
"runtime"
"sort"
"sync"
"time"
flag "github.com/spf13/pflag"
)
// DebugInfo gathers data for debugging.
type DebugInfo struct {
Version string `json:"version"`
Timezone string `json:"timezone"`
Architecture string `json:"architecture"`
Modules []string `json:"modules"`
ModulesAdditionalData map[string]map[string]any `json:"modules_additional_data"`
Flags map[string]any `json:"flags"`
}
// BuildDebug builds the debug data from modules.
func BuildDebug(ctx *Context) {
debugMu.Lock()
defer debugMu.Unlock()
debug = &DebugInfo{
Version: Version,
Timezone: time.Now().Location().String(),
Architecture: runtime.GOARCH,
Modules: make([]string, len(ctx.moduleInstances)),
ModulesAdditionalData: make(map[string]map[string]any),
Flags: make(map[string]any),
}
i := 0
for ID, mod := range ctx.moduleInstances {
debug.Modules[i] = ID
i++
debuggable, ok := mod.(Debuggable)
if !ok {
continue
}
debug.ModulesAdditionalData[ID] = debuggable.Debug()
}
sort.Sort(AlphanumericSort(debug.Modules))
ctx.ParsedFlags().VisitAll(func(f *flag.Flag) {
debug.Flags[f.Name] = f.Value.String()
})
}
// Debug returns the debug data.
func Debug() DebugInfo {
debugMu.Lock()
defer debugMu.Unlock()
if debug == nil {
return DebugInfo{}
}
return *debug
}
var (
debug *DebugInfo
debugMu sync.Mutex
)
================================================
FILE: pkg/gotenberg/debug_test.go
================================================
package gotenberg
import (
"reflect"
"runtime"
"testing"
flag "github.com/spf13/pflag"
)
func TestBuildDebug(t *testing.T) {
if !reflect.DeepEqual(Debug(), DebugInfo{}) {
t.Errorf("Debug() should return empty debug data")
}
t.Setenv("TZ", "UTC")
fs := flag.NewFlagSet("gotenberg", flag.ExitOnError)
fs.String("foo", "bar", "Set foo")
ctx := NewContext(ParsedFlags{
FlagSet: fs,
}, func() []ModuleDescriptor {
mod1 := &struct {
ModuleMock
}{}
mod1.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "foo", New: func() Module { return mod1 }}
}
mod2 := &struct {
ModuleMock
DebuggableMock
}{}
mod2.DescriptorMock = func() ModuleDescriptor {
return ModuleDescriptor{ID: "bar", New: func() Module { return mod2 }}
}
mod2.DebugMock = func() map[string]any {
return map[string]any{
"foo": "bar",
}
}
return []ModuleDescriptor{mod1.Descriptor(), mod2.Descriptor()}
}())
// Load modules.
_, err := ctx.Modules(new(Module))
if err != nil {
t.Errorf("expected no error but got: %v", err)
}
// Build debug data.
BuildDebug(ctx)
expect := DebugInfo{
Version: Version,
Timezone: "UTC",
Architecture: runtime.GOARCH,
Modules: []string{
"bar",
"foo",
},
ModulesAdditionalData: map[string]map[string]any{
"bar": {
"foo": "bar",
},
},
Flags: map[string]any{
"foo": "bar",
},
}
if !reflect.DeepEqual(expect, Debug()) {
t.Errorf("expected '%+v', bug got '%+v'", expect, Debug())
}
}
================================================
FILE: pkg/gotenberg/doc.go
================================================
// Package gotenberg provides most of the logic of the module system.
//
// caddyserver/caddy, licensed under the Apache License 2.0, has significantly
// inspired this module system.
//
// More details are available on https://caddyserver.com/.
package gotenberg
================================================
FILE: pkg/gotenberg/env.go
================================================
package gotenberg
import (
"fmt"
"os"
"strconv"
)
// StringEnv retrieves the value of the environment variable named by the key.
// If the variable is present in the environment and not empty, the value is
// returned.
func StringEnv(key string) (string, error) {
val, ok := os.LookupEnv(key)
if !ok {
return "", fmt.Errorf("environment variable '%s' does not exist", key)
}
if val == "" {
return "", fmt.Errorf("environment variable '%s' is empty", key)
}
return val, nil
}
// IntEnv relies on [StringEnv] and converts the values if it exists and is not
// empty.
func IntEnv(key string) (int, error) {
val, err := StringEnv(key)
if err != nil {
return 0, err
}
intVal, err := strconv.Atoi(val)
if err != nil {
return 0, fmt.Errorf("get int value of environment variable '%s': %w", key, err)
}
return intVal, nil
}
================================================
FILE: pkg/gotenberg/env_test.go
================================================
package gotenberg
import (
"os"
"testing"
)
func TestStringEnv(t *testing.T) {
for _, tc := range []struct {
scenario string
key string
setEnv func()
expectVal string
expectError bool
}{
{
scenario: "non-existing environment variable",
key: "NON_EXISTING",
expectVal: "",
expectError: true,
},
{
scenario: "empty environment variable",
key: "EMPTY_STRING",
setEnv: func() {
err := os.Setenv("EMPTY_STRING", "")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: "",
expectError: true,
},
{
scenario: "success",
key: "EXISTING_STRING_VALUE",
setEnv: func() {
err := os.Setenv("EXISTING_STRING_VALUE", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: "foo",
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.setEnv != nil {
tc.setEnv()
}
val, err := StringEnv(tc.key)
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")
}
if tc.expectVal != val {
t.Errorf("expected value '%s' but got '%s'", tc.expectVal, val)
}
})
}
}
func TestIntEnv(t *testing.T) {
for _, tc := range []struct {
scenario string
key string
setEnv func()
expectVal int
expectError bool
}{
{
scenario: "empty environment variable",
key: "EMPTY_INT",
setEnv: func() {
err := os.Setenv("EMPTY_INT", "")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: 0,
expectError: true,
},
{
scenario: "non-integer value",
key: "NON_INTEGER",
setEnv: func() {
err := os.Setenv("NON_INTEGER", "foo")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: 0,
expectError: true,
},
{
scenario: "success",
key: "EXISTING_INT_VALUE",
setEnv: func() {
err := os.Setenv("EXISTING_INT_VALUE", "123")
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
},
expectVal: 123,
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.setEnv != nil {
tc.setEnv()
}
val, err := IntEnv(tc.key)
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")
}
if tc.expectVal != val {
t.Errorf("expected value %d but got %d", tc.expectVal, val)
}
})
}
}
================================================
FILE: pkg/gotenberg/filter.go
================================================
package gotenberg
import (
"context"
"errors"
"fmt"
"time"
"github.com/dlclark/regexp2"
)
// ErrFiltered happens if a value is filtered by the [FilterDeadline] function.
var ErrFiltered = errors.New("value filtered")
// FilterDeadline checks if given value is allowed and not denied according to
// regex patterns. It returns a [context.DeadlineExceeded] if it takes too long
// to process.
func FilterDeadline(allowed, denied *regexp2.Regexp, s string, deadline time.Time) error {
// FIXME: not ideal to compile everytime, but is there another way to create a clone?
if allowed.String() != "" {
allow := regexp2.MustCompile(allowed.String(), 0)
allow.MatchTimeout = time.Until(deadline)
ok, err := allow.MatchString(s)
if err != nil {
if time.Now().After(deadline) {
return context.DeadlineExceeded
}
return fmt.Errorf("'%s' cannot handle '%s': %w", allow.String(), s, err)
}
if !ok {
return fmt.Errorf("'%s' does not match the expression from the allowed list: %w", s, ErrFiltered)
}
}
if denied.String() != "" {
deny := regexp2.MustCompile(denied.String(), 0)
deny.MatchTimeout = time.Until(deadline)
ok, err := deny.MatchString(s)
if err != nil {
if time.Now().After(deadline) {
return context.DeadlineExceeded
}
return fmt.Errorf("'%s' cannot handle '%s': %w", deny.String(), s, err)
}
if ok {
return fmt.Errorf("'%s' matches the expression from the denied list: %w", s, ErrFiltered)
}
}
return nil
}
================================================
FILE: pkg/gotenberg/filter_test.go
================================================
package gotenberg
import (
"context"
"errors"
"testing"
"time"
"github.com/dlclark/regexp2"
)
func TestFilterDeadline(t *testing.T) {
for _, tc := range []struct {
scenario string
allowed *regexp2.Regexp
denied *regexp2.Regexp
s string
deadline time.Time
expectError bool
expectedError error
}{
{
scenario: "DeadlineExceeded (allowed)",
allowed: regexp2.MustCompile("foo", 0),
denied: regexp2.MustCompile("", 0),
s: "foo",
deadline: time.Now().Add(time.Duration(-1) * time.Hour),
expectError: true,
expectedError: context.DeadlineExceeded,
},
{
scenario: "ErrFiltered (allowed)",
allowed: regexp2.MustCompile("foo", 0),
denied: regexp2.MustCompile("", 0),
s: "bar",
deadline: time.Now().Add(time.Duration(5) * time.Second),
expectError: true,
expectedError: ErrFiltered,
},
{
scenario: "DeadlineExceeded (denied)",
allowed: regexp2.MustCompile("", 0),
denied: regexp2.MustCompile("foo", 0),
s: "foo",
deadline: time.Now().Add(time.Duration(-1) * time.Hour),
expectError: true,
expectedError: context.DeadlineExceeded,
},
{
scenario: "ErrFiltered (denied)",
allowed: regexp2.MustCompile("", 0),
denied: regexp2.MustCompile("foo", 0),
s: "foo",
deadline: time.Now().Add(time.Duration(5) * time.Second),
expectError: true,
expectedError: ErrFiltered,
},
{
scenario: "success",
allowed: regexp2.MustCompile("", 0),
denied: regexp2.MustCompile("", 0),
s: "foo",
deadline: time.Now().Add(time.Duration(5) * time.Second),
expectError: false,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
err := FilterDeadline(tc.allowed, tc.denied, tc.s, tc.deadline)
if tc.expectError && err == nil {
t.Fatal("expected an error but got none")
}
if !tc.expectError && err != nil {
t.Fatalf("expected no error but got: %v", err)
}
if tc.expectedError != nil && !errors.Is(err, tc.expectedError) {
t.Fatalf("expected error %v but got: %v", tc.expectedError, err)
}
})
}
}
================================================
FILE: pkg/gotenberg/flags.go
================================================
package gotenberg
import (
"time"
"github.com/dlclark/regexp2"
"github.com/labstack/gommon/bytes"
flag "github.com/spf13/pflag"
)
// ParsedFlags wraps a [flag.FlagSet] so that retrieving the typed values is
// easier.
type ParsedFlags struct {
*flag.FlagSet
}
// MustString returns the string value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustString(name string) string {
val, err := f.GetString(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedString returns the string value of a deprecated flag if it was
// explicitly set or the string value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedString(deprecated string, newName string) string {
if f.Changed(deprecated) {
return f.MustString(deprecated)
}
return f.MustString(newName)
}
// MustStringSlice returns the string slice value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustStringSlice(name string) []string {
val, err := f.GetStringSlice(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedStringSlice returns the string slice value of a deprecated
// flag if it was explicitly set or the string slice value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedStringSlice(deprecated string, newName string) []string {
if f.Changed(deprecated) {
return f.MustStringSlice(deprecated)
}
return f.MustStringSlice(newName)
}
// MustBool returns the boolean value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustBool(name string) bool {
val, err := f.GetBool(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedBool returns the boolean value of a deprecated flag if it was
// explicitly set or the int value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedBool(deprecated string, newName string) bool {
if f.Changed(deprecated) {
return f.MustBool(deprecated)
}
return f.MustBool(newName)
}
// MustInt64 returns the int64 value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustInt64(name string) int64 {
val, err := f.GetInt64(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedInt64 returns the int64 value of a deprecated flag if it was
// explicitly set or the int64 value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedInt64(deprecated string, newName string) int64 {
if f.Changed(deprecated) {
return f.MustInt64(deprecated)
}
return f.MustInt64(newName)
}
// MustInt returns the int value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustInt(name string) int {
val, err := f.GetInt(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedInt returns the int value of a deprecated flag if it was
// explicitly set or the int value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedInt(deprecated string, newName string) int {
if f.Changed(deprecated) {
return f.MustInt(deprecated)
}
return f.MustInt(newName)
}
// MustFloat64 returns the float value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustFloat64(name string) float64 {
val, err := f.GetFloat64(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedFloat64 returns the float value of a deprecated flag if it was
// explicitly set or the float value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedFloat64(deprecated string, newName string) float64 {
if f.Changed(deprecated) {
return f.MustFloat64(deprecated)
}
return f.MustFloat64(newName)
}
// MustDuration returns the time.Duration value of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustDuration(name string) time.Duration {
val, err := f.GetDuration(name)
if err != nil {
panic(err)
}
return val
}
// MustDeprecatedDuration returns the time.Duration value of a deprecated flag
// if it was explicitly set or the time.Duration value of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedDuration(deprecated string, newName string) time.Duration {
if f.Changed(deprecated) {
return f.MustDuration(deprecated)
}
return f.MustDuration(newName)
}
// MustHumanReadableBytes returns the human-readable bytes string of a flag
// given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustHumanReadableBytes(name string) int64 {
val, err := f.GetString(name)
if err != nil {
panic(err)
}
if val == "" {
return 0
}
b, err := bytes.Parse(val)
if err != nil {
panic(err)
}
return b
}
// MustDeprecatedHumanReadableBytes returns the human-readable bytes of a
// deprecated flag if it was explicitly set or the human-readable bytes string
// of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedHumanReadableBytes(deprecated string, newName string) int64 {
if f.Changed(deprecated) {
return f.MustHumanReadableBytes(deprecated)
}
return f.MustHumanReadableBytes(newName)
}
// MustRegexp returns the regular expression of a flag given by name.
// It panics if an error occurs.
func (f *ParsedFlags) MustRegexp(name string) *regexp2.Regexp {
val, err := f.GetString(name)
if err != nil {
panic(err)
}
return regexp2.MustCompile(val, 0)
}
// MustDeprecatedRegexp returns the regular expression of a deprecated flag if
// it was explicitly set or the regular expression of the new flag.
// It panics if an error occurs.
func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName string) *regexp2.Regexp {
if f.Changed(deprecated) {
return f.MustRegexp(deprecated)
}
return f.MustRegexp(newName)
}
================================================
FILE: pkg/gotenberg/flags_test.go
================================================
package gotenberg
import (
"reflect"
"regexp"
"testing"
"time"
flag "github.com/spf13/pflag"
)
func TestParsedFlags_MustString(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.String("foo", "", "")
err := fs.Parse([]string{"--foo=foo"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for _, tc := range []struct {
scenario string
name string
expectPanic bool
}{
{
scenario: "success",
name: "foo",
expectPanic: false,
},
{
scenario: "non-existing flag",
name: "bar",
expectPanic: true,
},
} {
t.Run(tc.scenario, func(t *testing.T) {
if tc.expectPanic {
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic but got none")
}
}()
}
if !tc.expectPanic {
defer func() {
if r := recover(); r != nil {
t.Fatalf("expected no panic but got: %v", r)
}
}()
}
parsedFlags.MustString(tc.name)
})
}
}
func TestParsedFlags_MustDeprecatedString(t *testing.T) {
for _, tc := range []struct {
scenario string
rawFlags []string
expectValue string
}{
{
scenario: "deprecated flag value",
rawFlags: []string{"--foo=foo"},
expectValue: "foo",
},
{
scenario: "non-deprecated flag value",
rawFlags: []string{"--bar=bar"},
expectValue: "bar",
},
{
scenario: "deprecated flag value > non-deprecated flag value",
rawFlags: []string{"--foo=foo", "--bar=bar"},
expectValue: "foo",
},
} {
t.Run(tc.scenario, func(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.String("foo", "", "")
fs.String("bar", "", "")
parsedFlags := ParsedFlags{FlagSet: fs}
err := parsedFlags.Parse(tc.rawFlags)
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
actual := parsedFlags.MustDeprecatedString("foo", "bar")
if actual != tc.expectValue {
t.Errorf("expected '%s' but got '%s'", tc.expectValue, actual)
}
})
}
}
func TestParsedFlags_MustStringSlice(t *testing.T) {
fs := flag.NewFlagSet("tests", flag.ContinueOnError)
fs.StringSlice("foo", make([]string, 0), "")
err := fs.Parse([]string{"--foo=foo", "--foo=bar", "--foo=baz"})
if err != nil {
t.Fatalf("expected no error but got: %v", err)
}
parsedFlags := ParsedFlags{FlagSet: fs}
for _, tc := range []struct {
scenario string
name string
expectPanic bool
}{
{
scenario: "success",
name: "foo",
expectPanic: false,
},
{
scenario: "non-existing flag",
name: "bar",
expectPanic: true,
},
} {
t.Run(tc.scenario, func(t
gitextract_bfoz4ih4/
├── .bruno/
│ ├── Chromium/
│ │ ├── Convert/
│ │ │ ├── HTML to PDF.bru
│ │ │ ├── Markdown to PDF.bru
│ │ │ └── URL to PDF.bru
│ │ └── Screenshot/
│ │ ├── HTML Screenshot.bru
│ │ ├── Markdown Screenshot.bru
│ │ └── URL Screenshot.bru
│ ├── Health & Info/
│ │ ├── Debug.bru
│ │ ├── Health.bru
│ │ ├── Prometheus Metrics.bru
│ │ └── Version.bru
│ ├── LibreOffice/
│ │ └── Convert to PDF.bru
│ ├── PDF Engines/
│ │ ├── Bookmarks/
│ │ │ ├── Read Bookmarks.bru
│ │ │ └── Write Bookmarks.bru
│ │ ├── Convert/
│ │ │ └── Convert PDF.bru
│ │ ├── Embed/
│ │ │ └── Embed Files.bru
│ │ ├── Encrypt/
│ │ │ └── Encrypt PDF.bru
│ │ ├── Flatten/
│ │ │ └── Flatten PDF.bru
│ │ ├── Merge/
│ │ │ └── Merge PDFs.bru
│ │ ├── Metadata/
│ │ │ ├── Read Metadata.bru
│ │ │ └── Write Metadata.bru
│ │ ├── Rotate/
│ │ │ └── Rotate PDF.bru
│ │ ├── Split/
│ │ │ └── Split PDF.bru
│ │ ├── Stamp/
│ │ │ └── Stamp PDF.bru
│ │ └── Watermark/
│ │ └── Watermark PDF.bru
│ ├── bruno.json
│ ├── collection.bru
│ └── environments/
│ ├── Demo.bru
│ └── Local.bru
├── .dockerignore
├── .github/
│ ├── FUNDING.yml
│ ├── actions/
│ │ ├── build-test-push/
│ │ │ ├── action.yml
│ │ │ ├── build.sh
│ │ │ ├── push.sh
│ │ │ └── test.sh
│ │ ├── clean/
│ │ │ ├── action.yml
│ │ │ └── clean.sh
│ │ └── merge/
│ │ ├── action.yml
│ │ └── merge.sh
│ ├── dependabot.yml
│ ├── stale.yml
│ └── workflows/
│ ├── continuous-delivery.yml
│ ├── continuous-integration.yml
│ └── pull-request-cleanup.yml
├── .gitignore
├── .golangci.yml
├── .node-version
├── .prettierignore
├── .prettierrc
├── AGENTS.md
├── CLAUDE.md
├── CONTRIBUTING.md
├── GEMINI.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── build/
│ ├── Dockerfile
│ ├── Dockerfile.aws-lambda
│ ├── Dockerfile.cloudrun
│ ├── chromium-hyphen-data/
│ │ └── 120.0.6050.0/
│ │ ├── _metadata/
│ │ │ └── verified_contents.json
│ │ ├── hyph-af.hyb
│ │ ├── hyph-as.hyb
│ │ ├── hyph-be.hyb
│ │ ├── hyph-bg.hyb
│ │ ├── hyph-bn.hyb
│ │ ├── hyph-cs.hyb
│ │ ├── hyph-cu.hyb
│ │ ├── hyph-cy.hyb
│ │ ├── hyph-da.hyb
│ │ ├── hyph-de-1901.hyb
│ │ ├── hyph-de-1996.hyb
│ │ ├── hyph-de-ch-1901.hyb
│ │ ├── hyph-el.hyb
│ │ ├── hyph-en-gb.hyb
│ │ ├── hyph-en-us.hyb
│ │ ├── hyph-es.hyb
│ │ ├── hyph-et.hyb
│ │ ├── hyph-eu.hyb
│ │ ├── hyph-fr.hyb
│ │ ├── hyph-ga.hyb
│ │ ├── hyph-gl.hyb
│ │ ├── hyph-gu.hyb
│ │ ├── hyph-hi.hyb
│ │ ├── hyph-hr.hyb
│ │ ├── hyph-hu.hyb
│ │ ├── hyph-hy.hyb
│ │ ├── hyph-it.hyb
│ │ ├── hyph-ka.hyb
│ │ ├── hyph-kn.hyb
│ │ ├── hyph-la.hyb
│ │ ├── hyph-lt.hyb
│ │ ├── hyph-lv.hyb
│ │ ├── hyph-ml.hyb
│ │ ├── hyph-mn-cyrl.hyb
│ │ ├── hyph-mr.hyb
│ │ ├── hyph-mul-ethi.hyb
│ │ ├── hyph-nb.hyb
│ │ ├── hyph-nl.hyb
│ │ ├── hyph-nn.hyb
│ │ ├── hyph-or.hyb
│ │ ├── hyph-pa.hyb
│ │ ├── hyph-pt.hyb
│ │ ├── hyph-ru.hyb
│ │ ├── hyph-sk.hyb
│ │ ├── hyph-sl.hyb
│ │ ├── hyph-sq.hyb
│ │ ├── hyph-sv.hyb
│ │ ├── hyph-ta.hyb
│ │ ├── hyph-te.hyb
│ │ ├── hyph-tk.hyb
│ │ ├── hyph-uk.hyb
│ │ ├── hyph-und-ethi.hyb
│ │ └── manifest.json
│ └── fonts.conf
├── cmd/
│ ├── gotenberg/
│ │ └── main.go
│ └── gotenberg.go
├── go.mod
├── go.sum
├── package.json
├── pkg/
│ ├── gotenberg/
│ │ ├── cmd.go
│ │ ├── context.go
│ │ ├── context_test.go
│ │ ├── debug.go
│ │ ├── debug_test.go
│ │ ├── doc.go
│ │ ├── env.go
│ │ ├── env_test.go
│ │ ├── filter.go
│ │ ├── filter_test.go
│ │ ├── flags.go
│ │ ├── flags_test.go
│ │ ├── fs.go
│ │ ├── gc.go
│ │ ├── gc_test.go
│ │ ├── logging.go
│ │ ├── metrics.go
│ │ ├── mocks.go
│ │ ├── modules.go
│ │ ├── modules_test.go
│ │ ├── pdfengine.go
│ │ ├── shutdown.go
│ │ ├── sort.go
│ │ ├── sort_test.go
│ │ ├── supervisor.go
│ │ ├── supervisor_test.go
│ │ └── version.go
│ ├── modules/
│ │ ├── api/
│ │ │ ├── api.go
│ │ │ ├── context.go
│ │ │ ├── context_test.go
│ │ │ ├── doc.go
│ │ │ ├── errors.go
│ │ │ ├── errors_test.go
│ │ │ ├── formdata.go
│ │ │ ├── formdata_test.go
│ │ │ ├── middlewares.go
│ │ │ ├── mocks.go
│ │ │ └── testdata/
│ │ │ └── sample.txt
│ │ ├── chromium/
│ │ │ ├── browser.go
│ │ │ ├── chromium.go
│ │ │ ├── debug.go
│ │ │ ├── doc.go
│ │ │ ├── events.go
│ │ │ ├── events_test.go
│ │ │ ├── mocks.go
│ │ │ ├── routes.go
│ │ │ ├── stream.go
│ │ │ └── tasks.go
│ │ ├── exiftool/
│ │ │ ├── doc.go
│ │ │ └── exiftool.go
│ │ ├── libreoffice/
│ │ │ ├── api/
│ │ │ │ ├── api.go
│ │ │ │ ├── doc.go
│ │ │ │ ├── freeport.go
│ │ │ │ ├── libreoffice.go
│ │ │ │ └── mocks.go
│ │ │ ├── doc.go
│ │ │ ├── libreoffice.go
│ │ │ ├── pdfengine/
│ │ │ │ ├── doc.go
│ │ │ │ └── pdfengine.go
│ │ │ └── routes.go
│ │ ├── logging/
│ │ │ ├── color.go
│ │ │ ├── doc.go
│ │ │ ├── gcp.go
│ │ │ └── logging.go
│ │ ├── pdfcpu/
│ │ │ ├── doc.go
│ │ │ ├── pdfcpu.go
│ │ │ ├── sort.go
│ │ │ └── sort_test.go
│ │ ├── pdfengines/
│ │ │ ├── doc.go
│ │ │ ├── multi.go
│ │ │ ├── pdfengines.go
│ │ │ └── routes.go
│ │ ├── pdftk/
│ │ │ ├── doc.go
│ │ │ └── pdftk.go
│ │ ├── prometheus/
│ │ │ ├── doc.go
│ │ │ └── prometheus.go
│ │ ├── qpdf/
│ │ │ ├── doc.go
│ │ │ └── qpdf.go
│ │ └── webhook/
│ │ ├── client.go
│ │ ├── doc.go
│ │ ├── middleware.go
│ │ └── webhook.go
│ └── standard/
│ ├── doc.go
│ └── imports.go
└── test/
└── integration/
├── doc.go
├── features/
│ ├── chromium_concurrent.feature
│ ├── chromium_convert_html.feature
│ ├── chromium_convert_markdown.feature
│ ├── chromium_convert_url.feature
│ ├── debug.feature
│ ├── health.feature
│ ├── libreoffice_convert.feature
│ ├── output_filename.feature
│ ├── pdfengines_bookmarks.feature
│ ├── pdfengines_convert.feature
│ ├── pdfengines_embed.feature
│ ├── pdfengines_encrypt.feature
│ ├── pdfengines_flatten.feature
│ ├── pdfengines_merge.feature
│ ├── pdfengines_metadata.feature
│ ├── pdfengines_rotate.feature
│ ├── pdfengines_split.feature
│ ├── pdfengines_stamp.feature
│ ├── pdfengines_watermark.feature
│ ├── prometheus_metrics.feature
│ ├── root.feature
│ ├── version.feature
│ └── webhook.feature
├── main_test.go
├── scenario/
│ ├── compare.go
│ ├── containers.go
│ ├── doc.go
│ ├── http.go
│ ├── scenario.go
│ └── server.go
├── testdata/
│ ├── Special_Chars_ß.docx
│ ├── embed_1.xml
│ ├── embed_2.xml
│ ├── feature-rich-html/
│ │ └── index.html
│ ├── feature-rich-html-remote/
│ │ └── index.html
│ ├── feature-rich-markdown/
│ │ ├── index.html
│ │ └── table.md
│ ├── header-footer-html/
│ │ ├── footer.html
│ │ └── header.html
│ ├── page-1-html/
│ │ └── index.html
│ ├── page-1-markdown/
│ │ ├── index.html
│ │ └── page_1.md
│ ├── page_1.docx
│ ├── page_2.docx
│ ├── pages-12-html/
│ │ └── index.html
│ ├── pages-12-markdown/
│ │ ├── index.html
│ │ ├── page_1.md
│ │ ├── page_10.md
│ │ ├── page_11.md
│ │ ├── page_12.md
│ │ ├── page_2.md
│ │ ├── page_3.md
│ │ ├── page_4.md
│ │ ├── page_5.md
│ │ ├── page_6.md
│ │ ├── page_7.md
│ │ ├── page_8.md
│ │ └── page_9.md
│ ├── pages-3-html/
│ │ └── index.html
│ ├── pages-3-markdown/
│ │ ├── index.html
│ │ ├── page_1.md
│ │ ├── page_2.md
│ │ └── page_3.md
│ ├── pages_12.docx
│ ├── pages_3.docx
│ ├── pem/
│ │ ├── README.md
│ │ ├── cert.pem
│ │ └── key.pem
│ └── protected_page_1.docx
└── teststore/
└── .gitkeep
SYMBOL INDEX (757 symbols across 73 files)
FILE: cmd/gotenberg.go
constant banner (line 21) | banner = `
function Run (line 38) | func Run() {
FILE: cmd/gotenberg/main.go
function main (line 9) | func main() {
FILE: pkg/gotenberg/cmd.go
type Cmd (line 17) | type Cmd struct
method Start (line 60) | func (cmd *Cmd) Start() error {
method Wait (line 78) | func (cmd *Cmd) Wait() error {
method Exec (line 89) | func (cmd *Cmd) Exec() (int, error) {
method pipeOutput (line 137) | func (cmd *Cmd) pipeOutput() error {
method Kill (line 189) | func (cmd *Cmd) Kill() error {
function Command (line 28) | func Command(logger *zap.Logger, binPath string, args ...string) *Cmd {
function CommandContext (line 44) | func CommandContext(ctx context.Context, logger *zap.Logger, binPath str...
FILE: pkg/gotenberg/context.go
type Context (line 10) | type Context struct
method ParsedFlags (line 35) | func (ctx *Context) ParsedFlags() ParsedFlags {
method Module (line 48) | func (ctx *Context) Module(kind any) (any, error) {
method Modules (line 73) | func (ctx *Context) Modules(kind any) ([]any, error) {
method loadModule (line 104) | func (ctx *Context) loadModule(id string, instance any) error {
function NewContext (line 18) | func NewContext(
FILE: pkg/gotenberg/context_test.go
function TestContext_Module (line 8) | func TestContext_Module(t *testing.T) {
function TestContext_Modules (line 79) | func TestContext_Modules(t *testing.T) {
function TestContext_loadModule (line 151) | func TestContext_loadModule(t *testing.T) {
FILE: pkg/gotenberg/debug.go
type DebugInfo (line 13) | type DebugInfo struct
function BuildDebug (line 23) | func BuildDebug(ctx *Context) {
function Debug (line 57) | func Debug() DebugInfo {
FILE: pkg/gotenberg/debug_test.go
function TestBuildDebug (line 11) | func TestBuildDebug(t *testing.T) {
FILE: pkg/gotenberg/env.go
function StringEnv (line 12) | func StringEnv(key string) (string, error) {
function IntEnv (line 25) | func IntEnv(key string) (int, error) {
FILE: pkg/gotenberg/env_test.go
function TestStringEnv (line 8) | func TestStringEnv(t *testing.T) {
function TestIntEnv (line 69) | func TestIntEnv(t *testing.T) {
FILE: pkg/gotenberg/filter.go
function FilterDeadline (line 18) | func FilterDeadline(allowed, denied *regexp2.Regexp, s string, deadline ...
FILE: pkg/gotenberg/filter_test.go
function TestFilterDeadline (line 12) | func TestFilterDeadline(t *testing.T) {
FILE: pkg/gotenberg/flags.go
type ParsedFlags (line 13) | type ParsedFlags struct
method MustString (line 19) | func (f *ParsedFlags) MustString(name string) string {
method MustDeprecatedString (line 31) | func (f *ParsedFlags) MustDeprecatedString(deprecated string, newName ...
method MustStringSlice (line 41) | func (f *ParsedFlags) MustStringSlice(name string) []string {
method MustDeprecatedStringSlice (line 53) | func (f *ParsedFlags) MustDeprecatedStringSlice(deprecated string, new...
method MustBool (line 63) | func (f *ParsedFlags) MustBool(name string) bool {
method MustDeprecatedBool (line 75) | func (f *ParsedFlags) MustDeprecatedBool(deprecated string, newName st...
method MustInt64 (line 85) | func (f *ParsedFlags) MustInt64(name string) int64 {
method MustDeprecatedInt64 (line 97) | func (f *ParsedFlags) MustDeprecatedInt64(deprecated string, newName s...
method MustInt (line 107) | func (f *ParsedFlags) MustInt(name string) int {
method MustDeprecatedInt (line 119) | func (f *ParsedFlags) MustDeprecatedInt(deprecated string, newName str...
method MustFloat64 (line 129) | func (f *ParsedFlags) MustFloat64(name string) float64 {
method MustDeprecatedFloat64 (line 141) | func (f *ParsedFlags) MustDeprecatedFloat64(deprecated string, newName...
method MustDuration (line 151) | func (f *ParsedFlags) MustDuration(name string) time.Duration {
method MustDeprecatedDuration (line 163) | func (f *ParsedFlags) MustDeprecatedDuration(deprecated string, newNam...
method MustHumanReadableBytes (line 174) | func (f *ParsedFlags) MustHumanReadableBytes(name string) int64 {
method MustDeprecatedHumanReadableBytes (line 196) | func (f *ParsedFlags) MustDeprecatedHumanReadableBytes(deprecated stri...
method MustRegexp (line 206) | func (f *ParsedFlags) MustRegexp(name string) *regexp2.Regexp {
method MustDeprecatedRegexp (line 218) | func (f *ParsedFlags) MustDeprecatedRegexp(deprecated string, newName ...
FILE: pkg/gotenberg/flags_test.go
function TestParsedFlags_MustString (line 12) | func TestParsedFlags_MustString(t *testing.T) {
function TestParsedFlags_MustDeprecatedString (line 61) | func TestParsedFlags_MustDeprecatedString(t *testing.T) {
function TestParsedFlags_MustStringSlice (line 103) | func TestParsedFlags_MustStringSlice(t *testing.T) {
function TestParsedFlags_MustDeprecatedStringSlice (line 152) | func TestParsedFlags_MustDeprecatedStringSlice(t *testing.T) {
function TestParsedFlags_MustBool (line 194) | func TestParsedFlags_MustBool(t *testing.T) {
function TestParsedFlags_MustDeprecatedBool (line 243) | func TestParsedFlags_MustDeprecatedBool(t *testing.T) {
function TestParsedFlags_MustInt64 (line 285) | func TestParsedFlags_MustInt64(t *testing.T) {
function TestParsedFlags_MustDeprecatedInt64 (line 334) | func TestParsedFlags_MustDeprecatedInt64(t *testing.T) {
function TestParsedFlags_MustInt (line 374) | func TestParsedFlags_MustInt(t *testing.T) {
function TestParsedFlags_MustDeprecatedInt (line 423) | func TestParsedFlags_MustDeprecatedInt(t *testing.T) {
function TestParsedFlags_MustFloat64 (line 465) | func TestParsedFlags_MustFloat64(t *testing.T) {
function TestParsedFlags_MustDeprecatedFloat64 (line 514) | func TestParsedFlags_MustDeprecatedFloat64(t *testing.T) {
function TestParsedFlags_MustDuration (line 556) | func TestParsedFlags_MustDuration(t *testing.T) {
function TestParsedFlags_MustDeprecatedDuration (line 605) | func TestParsedFlags_MustDeprecatedDuration(t *testing.T) {
function TestParsedFlags_MustHumanReadableBytes (line 647) | func TestParsedFlags_MustHumanReadableBytes(t *testing.T) {
function TestParsedFlags_MustDeprecatedHumanReadableBytes (line 703) | func TestParsedFlags_MustDeprecatedHumanReadableBytes(t *testing.T) {
function TestParsedFlags_MustRegexp (line 745) | func TestParsedFlags_MustRegexp(t *testing.T) {
function TestParsedFlags_MustDeprecatedRegexp (line 795) | func TestParsedFlags_MustDeprecatedRegexp(t *testing.T) {
FILE: pkg/gotenberg/fs.go
type MkdirAll (line 13) | type MkdirAll interface
type OsMkdirAll (line 19) | type OsMkdirAll struct
method MkdirAll (line 22) | func (o *OsMkdirAll) MkdirAll(path string, perm os.FileMode) error { r...
type PathRename (line 27) | type PathRename interface
type OsPathRename (line 33) | type OsPathRename struct
method Rename (line 36) | func (o *OsPathRename) Rename(oldpath, newpath string) error {
type FileSystem (line 43) | type FileSystem struct
method WorkingDir (line 58) | func (fs *FileSystem) WorkingDir() string {
method WorkingDirPath (line 64) | func (fs *FileSystem) WorkingDirPath() string {
method NewDirPath (line 70) | func (fs *FileSystem) NewDirPath() string {
method MkdirAll (line 76) | func (fs *FileSystem) MkdirAll() (string, error) {
function NewFileSystem (line 50) | func NewFileSystem(mkdirAll MkdirAll) *FileSystem {
FILE: pkg/gotenberg/gc.go
function GarbageCollect (line 15) | func GarbageCollect(logger *zap.Logger, rootPath string, includeSubstr [...
FILE: pkg/gotenberg/gc_test.go
function TestGarbageCollect (line 14) | func TestGarbageCollect(t *testing.T) {
FILE: pkg/gotenberg/logging.go
type LoggerProvider (line 17) | type LoggerProvider interface
type LeveledLogger (line 23) | type LeveledLogger struct
method Error (line 35) | func (leveled LeveledLogger) Error(msg string, keysAndValues ...any) {
method Warn (line 40) | func (leveled LeveledLogger) Warn(msg string, keysAndValues ...any) {
method Info (line 45) | func (leveled LeveledLogger) Info(msg string, keysAndValues ...any) {
method Debug (line 50) | func (leveled LeveledLogger) Debug(msg string, keysAndValues ...any) {
function NewLeveledLogger (line 28) | func NewLeveledLogger(logger *zap.Logger) *LeveledLogger {
FILE: pkg/gotenberg/metrics.go
type Metric (line 4) | type Metric struct
type MetricsProvider (line 24) | type MetricsProvider interface
FILE: pkg/gotenberg/mocks.go
type ModuleMock (line 11) | type ModuleMock struct
method Descriptor (line 15) | func (mod *ModuleMock) Descriptor() ModuleDescriptor {
type ProvisionerMock (line 20) | type ProvisionerMock struct
method Provision (line 24) | func (mod *ProvisionerMock) Provision(ctx *Context) error {
type ValidatorMock (line 29) | type ValidatorMock struct
method Validate (line 33) | func (mod *ValidatorMock) Validate() error {
type DebuggableMock (line 37) | type DebuggableMock struct
method Debug (line 41) | func (mod *DebuggableMock) Debug() map[string]any {
type PdfEngineMock (line 48) | type PdfEngineMock struct
method Merge (line 65) | func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Lo...
method Split (line 69) | func (engine *PdfEngineMock) Split(ctx context.Context, logger *zap.Lo...
method Flatten (line 73) | func (engine *PdfEngineMock) Flatten(ctx context.Context, logger *zap....
method Convert (line 77) | func (engine *PdfEngineMock) Convert(ctx context.Context, logger *zap....
method ReadMetadata (line 81) | func (engine *PdfEngineMock) ReadMetadata(ctx context.Context, logger ...
method PageCount (line 85) | func (engine *PdfEngineMock) PageCount(ctx context.Context, logger *za...
method WriteMetadata (line 89) | func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger...
method ReadBookmarks (line 93) | func (engine *PdfEngineMock) ReadBookmarks(ctx context.Context, logger...
method Encrypt (line 97) | func (engine *PdfEngineMock) Encrypt(ctx context.Context, logger *zap....
method EmbedFiles (line 101) | func (engine *PdfEngineMock) EmbedFiles(ctx context.Context, logger *z...
method WriteBookmarks (line 105) | func (engine *PdfEngineMock) WriteBookmarks(ctx context.Context, logge...
method Watermark (line 109) | func (engine *PdfEngineMock) Watermark(ctx context.Context, logger *za...
method Stamp (line 113) | func (engine *PdfEngineMock) Stamp(ctx context.Context, logger *zap.Lo...
method Rotate (line 117) | func (engine *PdfEngineMock) Rotate(ctx context.Context, logger *zap.L...
type PdfEngineProviderMock (line 122) | type PdfEngineProviderMock struct
method PdfEngine (line 126) | func (provider *PdfEngineProviderMock) PdfEngine() (PdfEngine, error) {
type ProcessMock (line 131) | type ProcessMock struct
method Start (line 137) | func (p *ProcessMock) Start(logger *zap.Logger) error {
method Stop (line 141) | func (p *ProcessMock) Stop(logger *zap.Logger) error {
method Healthy (line 145) | func (p *ProcessMock) Healthy(logger *zap.Logger) bool {
type ProcessSupervisorMock (line 150) | type ProcessSupervisorMock struct
method Launch (line 159) | func (s *ProcessSupervisorMock) Launch() error {
method Shutdown (line 163) | func (s *ProcessSupervisorMock) Shutdown() error {
method Healthy (line 167) | func (s *ProcessSupervisorMock) Healthy() bool {
method Run (line 171) | func (s *ProcessSupervisorMock) Run(ctx context.Context, logger *zap.L...
method ReqQueueSize (line 175) | func (s *ProcessSupervisorMock) ReqQueueSize() int64 {
method RestartsCount (line 179) | func (s *ProcessSupervisorMock) RestartsCount() int64 {
type LoggerProviderMock (line 184) | type LoggerProviderMock struct
method Logger (line 188) | func (provider *LoggerProviderMock) Logger(mod Module) (*zap.Logger, e...
type MetricsProviderMock (line 193) | type MetricsProviderMock struct
method Metrics (line 197) | func (provider *MetricsProviderMock) Metrics() ([]Metric, error) {
type MkdirAllMock (line 202) | type MkdirAllMock struct
method MkdirAll (line 206) | func (mkdirAll *MkdirAllMock) MkdirAll(path string, perm os.FileMode) ...
type PathRenameMock (line 211) | type PathRenameMock struct
method Rename (line 215) | func (rename *PathRenameMock) Rename(oldpath, newpath string) error {
FILE: pkg/gotenberg/modules.go
type Module (line 31) | type Module interface
type ModuleDescriptor (line 36) | type ModuleDescriptor struct
type Provisioner (line 52) | type Provisioner interface
type Validator (line 58) | type Validator interface
type App (line 64) | type App interface
type SystemLogger (line 74) | type SystemLogger interface
type Debuggable (line 80) | type Debuggable interface
function MustRegisterModule (line 99) | func MustRegisterModule(mod Module) {
function GetModuleDescriptors (line 125) | func GetModuleDescriptors() []ModuleDescriptor {
FILE: pkg/gotenberg/modules_test.go
function TestMustRegisterModule (line 8) | func TestMustRegisterModule(t *testing.T) {
function TestGetModuleDescriptors (line 81) | func TestGetModuleDescriptors(t *testing.T) {
FILE: pkg/gotenberg/pdfengine.go
type PdfEngineInvalidArgsError (line 44) | type PdfEngineInvalidArgsError struct
method Error (line 50) | func (e *PdfEngineInvalidArgsError) Error() string {
function NewPdfEngineInvalidArgs (line 56) | func NewPdfEngineInvalidArgs(engine, msg string) error {
constant StampSourceText (line 62) | StampSourceText string = "text"
constant StampSourceImage (line 65) | StampSourceImage string = "image"
constant StampSourcePDF (line 68) | StampSourcePDF string = "pdf"
type Stamp (line 72) | type Stamp struct
constant SplitModeIntervals (line 90) | SplitModeIntervals string = "intervals"
constant SplitModePages (line 94) | SplitModePages string = "pages"
type SplitMode (line 98) | type SplitMode struct
constant PdfA1a (line 113) | PdfA1a string = "PDF/A-1a"
constant PdfA1b (line 116) | PdfA1b string = "PDF/A-1b"
constant PdfA2a (line 119) | PdfA2a string = "PDF/A-2a"
constant PdfA2b (line 122) | PdfA2b string = "PDF/A-2b"
constant PdfA2u (line 125) | PdfA2u string = "PDF/A-2u"
constant PdfA3a (line 128) | PdfA3a string = "PDF/A-3a"
constant PdfA3b (line 131) | PdfA3b string = "PDF/A-3b"
constant PdfA3u (line 134) | PdfA3u string = "PDF/A-3u"
type PdfFormats (line 138) | type PdfFormats struct
type Bookmark (line 149) | type Bookmark struct
type PdfEngine (line 160) | type PdfEngine interface
type PdfEngineProvider (line 225) | type PdfEngineProvider interface
FILE: pkg/gotenberg/sort.go
type numberLoc (line 10) | type numberLoc
constant numberNone (line 13) | numberNone numberLoc = iota
constant numberPrefix (line 14) | numberPrefix
constant numberExtSuffix (line 15) | numberExtSuffix
constant numberSuffix (line 16) | numberSuffix
type AlphanumericSort (line 26) | type AlphanumericSort
method Len (line 28) | func (s AlphanumericSort) Len() int {
method Swap (line 32) | func (s AlphanumericSort) Swap(i, j int) {
method Less (line 36) | func (s AlphanumericSort) Less(i, j int) bool {
function extractNumber (line 81) | func extractNumber(str string) (int, string, numberLoc) {
FILE: pkg/gotenberg/sort_test.go
function TestAlphanumericSort (line 9) | func TestAlphanumericSort(t *testing.T) {
FILE: pkg/gotenberg/supervisor.go
type Process (line 27) | type Process interface
type ProcessSupervisor (line 48) | type ProcessSupervisor interface
type processSupervisor (line 78) | type processSupervisor struct
method Launch (line 119) | func (s *processSupervisor) Launch() error {
method Shutdown (line 132) | func (s *processSupervisor) Shutdown() error {
method restart (line 144) | func (s *processSupervisor) restart() error {
method Healthy (line 165) | func (s *processSupervisor) Healthy() bool {
method Run (line 179) | func (s *processSupervisor) Run(ctx context.Context, logger *zap.Logge...
method doRestart (line 288) | func (s *processSupervisor) doRestart(ctx context.Context) error {
method doRestartLocked (line 296) | func (s *processSupervisor) doRestartLocked(ctx context.Context) error {
method runWithDeadline (line 327) | func (s *processSupervisor) runWithDeadline(ctx context.Context, task ...
method ReqQueueSize (line 343) | func (s *processSupervisor) ReqQueueSize() int64 {
method RestartsCount (line 347) | func (s *processSupervisor) RestartsCount() int64 {
function NewProcessSupervisor (line 97) | func NewProcessSupervisor(logger *zap.Logger, process Process, maxReqLim...
FILE: pkg/gotenberg/supervisor_test.go
function TestProcessSupervisor_Launch (line 14) | func TestProcessSupervisor_Launch(t *testing.T) {
function TestProcessSupervisor_Shutdown (line 71) | func TestProcessSupervisor_Shutdown(t *testing.T) {
function TestProcessSupervisor_restart (line 111) | func TestProcessSupervisor_restart(t *testing.T) {
function TestProcessSupervisor_Healthy (line 164) | func TestProcessSupervisor_Healthy(t *testing.T) {
function TestProcessSupervisor_Run (line 222) | func TestProcessSupervisor_Run(t *testing.T) {
function TestProcessSupervisor_runWithDeadline (line 456) | func TestProcessSupervisor_runWithDeadline(t *testing.T) {
function TestProcessSupervisor_ReqQueueSize (line 498) | func TestProcessSupervisor_ReqQueueSize(t *testing.T) {
function TestProcessSupervisor_RestartsCount (line 555) | func TestProcessSupervisor_RestartsCount(t *testing.T) {
function TestProcessSupervisor_ConcurrentRun (line 622) | func TestProcessSupervisor_ConcurrentRun(t *testing.T) {
function TestProcessSupervisor_RestartDrainsAllSlots (line 686) | func TestProcessSupervisor_RestartDrainsAllSlots(t *testing.T) {
FILE: pkg/modules/api/api.go
function init (line 25) | func init() {
type Api (line 31) | type Api struct
method Descriptor (line 179) | func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 208) | func (a *Api) Provision(ctx *gotenberg.Context) error {
method Validate (line 350) | func (a *Api) Validate() error {
method Start (line 434) | func (a *Api) Start() error {
method StartupMessage (line 611) | func (a *Api) StartupMessage() string {
method Stop (line 620) | func (a *Api) Stop(ctx context.Context) error {
type downloadFromConfig (line 57) | type downloadFromConfig struct
type Router (line 65) | type Router interface
type Route (line 70) | type Route struct
type MiddlewareProvider (line 93) | type MiddlewareProvider interface
type MiddlewareStack (line 99) | type MiddlewareStack
constant DefaultStack (line 102) | DefaultStack MiddlewareStack = iota
constant PreRouterStack (line 103) | PreRouterStack
constant MultipartStack (line 104) | MultipartStack
type MiddlewarePriority (line 109) | type MiddlewarePriority
constant VeryLowPriority (line 112) | VeryLowPriority MiddlewarePriority = iota
constant LowPriority (line 113) | LowPriority
constant MediumPriority (line 114) | MediumPriority
constant HighPriority (line 115) | HighPriority
constant VeryHighPriority (line 116) | VeryHighPriority
type Middleware (line 144) | type Middleware struct
type HealthChecker (line 165) | type HealthChecker interface
type AsynchronousCounter (line 174) | type AsynchronousCounter interface
FILE: pkg/modules/api/context.go
type Context (line 40) | type Context struct
method Request (line 419) | func (ctx *Context) Request() *http.Request {
method FormData (line 424) | func (ctx *Context) FormData() *FormData {
method GeneratePath (line 435) | func (ctx *Context) GeneratePath(extension string) string {
method GeneratePathFromFilename (line 442) | func (ctx *Context) GeneratePathFromFilename(filename string) string {
method CreateSubDirectory (line 448) | func (ctx *Context) CreateSubDirectory(dirName string) (string, error) {
method Rename (line 459) | func (ctx *Context) Rename(oldpath, newpath string) error {
method AddOutputPaths (line 470) | func (ctx *Context) AddOutputPaths(paths ...string) error {
method Log (line 487) | func (ctx *Context) Log() *zap.Logger {
method BuildOutputFile (line 493) | func (ctx *Context) BuildOutputFile() (string, error) {
method OutputFilename (line 542) | func (ctx *Context) OutputFilename(outputPath string) string {
type trackingReader (line 55) | type trackingReader struct
method Read (line 60) | func (t *trackingReader) Read(p []byte) (int, error) {
type downloadFrom (line 77) | type downloadFrom struct
function newContext (line 95) | func newContext(echoCtx echo.Context, logger *zap.Logger, fs *gotenberg....
FILE: pkg/modules/api/context_test.go
function TestNewContext_Cancellation (line 20) | func TestNewContext_Cancellation(t *testing.T) {
FILE: pkg/modules/api/errors.go
type HttpError (line 6) | type HttpError interface
type SentinelHttpError (line 11) | type SentinelHttpError struct
method Error (line 27) | func (err SentinelHttpError) Error() string {
method HttpError (line 32) | func (err SentinelHttpError) HttpError() (int, string) {
function NewSentinelHttpError (line 19) | func NewSentinelHttpError(status int, message string) SentinelHttpError {
type sentinelWrappedError (line 38) | type sentinelWrappedError struct
method Is (line 43) | func (w sentinelWrappedError) Is(err error) bool {
method HttpError (line 47) | func (w sentinelWrappedError) HttpError() (int, string) {
function WrapError (line 64) | func WrapError(err error, sentinel SentinelHttpError) error {
FILE: pkg/modules/api/errors_test.go
function TestNewSentinelHttpError (line 10) | func TestNewSentinelHttpError(t *testing.T) {
function TestSentinelHttpError_Error (line 22) | func TestSentinelHttpError_Error(t *testing.T) {
function TestSentinelHttpError_HttpError (line 35) | func TestSentinelHttpError_HttpError(t *testing.T) {
function TestSentinelWrappedError_Is (line 53) | func TestSentinelWrappedError_Is(t *testing.T) {
function TestSentinelWrappedError_HttpError (line 66) | func TestSentinelWrappedError_HttpError(t *testing.T) {
function TestWrapError (line 89) | func TestWrapError(t *testing.T) {
FILE: pkg/modules/api/formdata.go
constant EmbedsFormField (line 22) | EmbedsFormField string = "embeds"
constant WatermarkFormField (line 25) | WatermarkFormField string = "watermark"
constant StampFormField (line 28) | StampFormField string = "stamp"
type FormData (line 35) | type FormData struct
method Validate (line 51) | func (form *FormData) Validate() error {
method String (line 67) | func (form *FormData) String(key string, target *string, defaultValue ...
method MandatoryString (line 77) | func (form *FormData) MandatoryString(key string, target *string) *For...
method Bool (line 87) | func (form *FormData) Bool(key string, target *bool, defaultValue bool...
method MandatoryBool (line 97) | func (form *FormData) MandatoryBool(key string, target *bool) *FormData {
method Int (line 107) | func (form *FormData) Int(key string, target *int, defaultValue int) *...
method MandatoryInt (line 117) | func (form *FormData) MandatoryInt(key string, target *int) *FormData {
method Float64 (line 127) | func (form *FormData) Float64(key string, target *float64, defaultValu...
method MandatoryFloat64 (line 137) | func (form *FormData) MandatoryFloat64(key string, target *float64) *F...
method Duration (line 147) | func (form *FormData) Duration(key string, target *time.Duration, defa...
method MandatoryDuration (line 158) | func (form *FormData) MandatoryDuration(key string, target *time.Durat...
method Inches (line 168) | func (form *FormData) Inches(key string, target *float64, defaultValue...
method MandatoryInches (line 183) | func (form *FormData) MandatoryInches(key string, target *float64) *Fo...
method inches (line 195) | func (form *FormData) inches(key string, target *float64) *FormData {
method Custom (line 265) | func (form *FormData) Custom(key string, assign func(value string) err...
method MandatoryCustom (line 292) | func (form *FormData) MandatoryCustom(key string, assign func(value st...
method Path (line 315) | func (form *FormData) Path(filename string, target *string) *FormData {
method MandatoryPath (line 325) | func (form *FormData) MandatoryPath(filename string, target *string) *...
method Content (line 334) | func (form *FormData) Content(filename string, target *string, default...
method MandatoryContent (line 353) | func (form *FormData) MandatoryContent(filename string, target *string...
method Paths (line 370) | func (form *FormData) Paths(extensions []string, target *[]string) *Fo...
method Embeds (line 381) | func (form *FormData) Embeds(target *[]string) *FormData {
method MandatoryPaths (line 401) | func (form *FormData) MandatoryPaths(extensions []string, target *[]st...
method Watermark (line 418) | func (form *FormData) Watermark(target *string) *FormData {
method Stamp (line 433) | func (form *FormData) Stamp(target *string) *FormData {
method paths (line 448) | func (form *FormData) paths(extensions []string, target *[]string) *Fo...
method append (line 481) | func (form *FormData) append(err error) {
method mustValue (line 488) | func (form *FormData) mustValue(key string, target any, defaultValue a...
method mustMandatoryField (line 517) | func (form *FormData) mustMandatoryField(key string, target any) *Form...
method mustAssign (line 536) | func (form *FormData) mustAssign(key, value string, target any) *FormD...
method path (line 564) | func (form *FormData) path(filename string, target *string) *FormData {
method mandatoryPath (line 579) | func (form *FormData) mandatoryPath(filename string, target *string) *...
method readFile (line 595) | func (form *FormData) readFile(path, filename string, target *string) ...
FILE: pkg/modules/api/formdata_test.go
function TestFormData_Validate (line 12) | func TestFormData_Validate(t *testing.T) {
function TestFormData_String (line 47) | func TestFormData_String(t *testing.T) {
function TestFormData_MandatoryString (line 107) | func TestFormData_MandatoryString(t *testing.T) {
function TestFormData_Bool (line 165) | func TestFormData_Bool(t *testing.T) {
function TestFormData_MandatoryBool (line 247) | func TestFormData_MandatoryBool(t *testing.T) {
function TestFormData_Int (line 317) | func TestFormData_Int(t *testing.T) {
function TestFormData_MandatoryInt (line 399) | func TestFormData_MandatoryInt(t *testing.T) {
function TestFormData_Float64 (line 469) | func TestFormData_Float64(t *testing.T) {
function TestFormData_MandatoryFloat64 (line 551) | func TestFormData_MandatoryFloat64(t *testing.T) {
function TestFormData_Duration (line 621) | func TestFormData_Duration(t *testing.T) {
function TestFormData_MandatoryDuration (line 702) | func TestFormData_MandatoryDuration(t *testing.T) {
function TestFormData_Inches (line 772) | func TestFormData_Inches(t *testing.T) {
function TestFormData_MandatoryInches (line 945) | func TestFormData_MandatoryInches(t *testing.T) {
function TestFormData_Custom (line 1099) | func TestFormData_Custom(t *testing.T) {
function TestFormData_MandatoryCustom (line 1194) | func TestFormData_MandatoryCustom(t *testing.T) {
function TestFormData_Path (line 1271) | func TestFormData_Path(t *testing.T) {
function TestFormData_MandatoryPath (line 1317) | func TestFormData_MandatoryPath(t *testing.T) {
function TestFormData_Content (line 1371) | func TestFormData_Content(t *testing.T) {
function TestFormData_MandatoryContent (line 1481) | func TestFormData_MandatoryContent(t *testing.T) {
function TestFormData_Paths (line 1572) | func TestFormData_Paths(t *testing.T) {
function TestFormData_MandatoryPaths (line 1654) | func TestFormData_MandatoryPaths(t *testing.T) {
function TestFormData_append (line 1726) | func TestFormData_append(t *testing.T) {
function TestFormData_mustValue (line 1735) | func TestFormData_mustValue(t *testing.T) {
function TestFormData_mustAssign (line 1749) | func TestFormData_mustAssign(t *testing.T) {
function TestFormData_Embeds (line 1762) | func TestFormData_Embeds(t *testing.T) {
FILE: pkg/modules/api/middlewares.go
function ParseError (line 33) | func ParseError(err error) (int, string) {
function httpErrorHandler (line 88) | func httpErrorHandler() echo.HTTPErrorHandler {
function latencyMiddleware (line 106) | func latencyMiddleware() echo.MiddlewareFunc {
function rootPathMiddleware (line 131) | func rootPathMiddleware(rootPath string) echo.MiddlewareFunc {
function traceMiddleware (line 147) | func traceMiddleware(header string) echo.MiddlewareFunc {
function outputFilenameMiddleware (line 171) | func outputFilenameMiddleware() echo.MiddlewareFunc {
function loggerMiddleware (line 190) | func loggerMiddleware(logger *zap.Logger, disableLoggingForPaths []strin...
function basicAuthMiddleware (line 267) | func basicAuthMiddleware(username, password string) echo.MiddlewareFunc {
function contextMiddleware (line 284) | func contextMiddleware(fs *gotenberg.FileSystem, timeout time.Duration, ...
function hardTimeoutMiddleware (line 344) | func hardTimeoutMiddleware(hardTimeout time.Duration) echo.MiddlewareFunc {
FILE: pkg/modules/api/mocks.go
type ContextMock (line 14) | type ContextMock struct
method SetDirPath (line 22) | func (ctx *ContextMock) SetDirPath(path string) {
method DirPath (line 31) | func (ctx *ContextMock) DirPath() string {
method SetValues (line 43) | func (ctx *ContextMock) SetValues(values map[string][]string) {
method SetFiles (line 53) | func (ctx *ContextMock) SetFiles(files map[string]string) {
method SetCancelled (line 61) | func (ctx *ContextMock) SetCancelled(cancelled bool) {
method OutputPaths (line 69) | func (ctx *ContextMock) OutputPaths() []string {
method SetLogger (line 77) | func (ctx *ContextMock) SetLogger(logger *zap.Logger) {
method SetEchoContext (line 85) | func (ctx *ContextMock) SetEchoContext(c echo.Context) {
method SetMkdirAll (line 93) | func (ctx *ContextMock) SetMkdirAll(mkdirAll gotenberg.MkdirAll) {
method SetPathRename (line 101) | func (ctx *ContextMock) SetPathRename(rename gotenberg.PathRename) {
type RouterMock (line 106) | type RouterMock struct
method Routes (line 110) | func (router *RouterMock) Routes() ([]Route, error) {
type MiddlewareProviderMock (line 115) | type MiddlewareProviderMock struct
method Middlewares (line 119) | func (provider *MiddlewareProviderMock) Middlewares() ([]Middleware, e...
type HealthCheckerMock (line 124) | type HealthCheckerMock struct
method Checks (line 129) | func (mod *HealthCheckerMock) Checks() ([]health.CheckerOption, error) {
method Ready (line 133) | func (mod *HealthCheckerMock) Ready() error {
FILE: pkg/modules/chromium/browser.go
type browser (line 26) | type browser interface
type browserArguments (line 32) | type browserArguments struct
type chromiumBrowser (line 52) | type chromiumBrowser struct
method Start (line 75) | func (b *chromiumBrowser) Start(logger *zap.Logger) error {
method Stop (line 167) | func (b *chromiumBrowser) Stop(logger *zap.Logger) error {
method Healthy (line 242) | func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
method pdf (line 276) | func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger,...
method screenshot (line 302) | func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap....
method do (line 329) | func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, ...
function newChromiumBrowser (line 64) | func newChromiumBrowser(arguments browserArguments) browser {
FILE: pkg/modules/chromium/chromium.go
function init (line 23) | func init() {
type Chromium (line 88) | type Chromium struct
method Descriptor (line 408) | func (mod *Chromium) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 445) | func (mod *Chromium) Provision(ctx *gotenberg.Context) error {
method Validate (line 509) | func (mod *Chromium) Validate() error {
method Start (line 529) | func (mod *Chromium) Start() error {
method StartupMessage (line 543) | func (mod *Chromium) StartupMessage() string {
method Stop (line 552) | func (mod *Chromium) Stop(ctx context.Context) error {
method Debug (line 568) | func (mod *Chromium) Debug() map[string]any {
method Metrics (line 585) | func (mod *Chromium) Metrics() ([]gotenberg.Metric, error) {
method Checks (line 605) | func (mod *Chromium) Checks() ([]health.CheckerOption, error) {
method Ready (line 621) | func (mod *Chromium) Ready() error {
method Chromium (line 650) | func (mod *Chromium) Chromium() (Api, error) {
method Routes (line 655) | func (mod *Chromium) Routes() ([]api.Route, error) {
method Pdf (line 671) | func (mod *Chromium) Pdf(ctx context.Context, logger *zap.Logger, url,...
method Screenshot (line 679) | func (mod *Chromium) Screenshot(ctx context.Context, logger *zap.Logge...
type Options (line 101) | type Options struct
type EmulatedMediaFeature (line 179) | type EmulatedMediaFeature struct
function DefaultOptions (line 191) | func DefaultOptions() Options {
type PdfOptions (line 213) | type PdfOptions struct
function DefaultPdfOptions (line 280) | func DefaultPdfOptions() PdfOptions {
type ScreenshotOptions (line 304) | type ScreenshotOptions struct
function DefaultScreenshotOptions (line 330) | func DefaultScreenshotOptions() ScreenshotOptions {
type Cookie (line 344) | type Cookie struct
type ExtraHttpHeader (line 375) | type ExtraHttpHeader struct
type Api (line 391) | type Api interface
type Provider (line 403) | type Provider interface
FILE: pkg/modules/chromium/debug.go
type debugLogger (line 12) | type debugLogger struct
method Write (line 17) | func (debug *debugLogger) Write(p []byte) (n int, err error) {
method Printf (line 24) | func (debug *debugLogger) Printf(format string, v ...any) {
FILE: pkg/modules/chromium/events.go
type eventRequestPausedOptions (line 26) | type eventRequestPausedOptions struct
function listenForEventRequestPaused (line 35) | func listenForEventRequestPaused(ctx context.Context, logger *zap.Logger...
type eventResponseReceivedOptions (line 142) | type eventResponseReceivedOptions struct
function listenForEventResponseReceived (line 158) | func listenForEventResponseReceived(
function shouldCheckResourceHttpStatusCode (line 215) | func shouldCheckResourceHttpStatusCode(rawURL string, ignoreDomains []st...
function hostnameFromURL (line 225) | func hostnameFromURL(rawURL string) string {
function normalizeDomains (line 233) | func normalizeDomains(domains []string) []string {
function normalizeDomain (line 247) | func normalizeDomain(domain string) string {
function matchesAnyDomain (line 274) | func matchesAnyDomain(host string, domains []string) bool {
type eventLoadingFailedOptions (line 288) | type eventLoadingFailedOptions struct
function listenForEventLoadingFailed (line 301) | func listenForEventLoadingFailed(ctx context.Context, logger *zap.Logger...
function listenForEventExceptionThrown (line 357) | func listenForEventExceptionThrown(ctx context.Context, logger *zap.Logg...
function waitForEventDomContentEventFired (line 373) | func waitForEventDomContentEventFired(ctx context.Context, logger *zap.L...
function waitForEventLoadEventFired (line 397) | func waitForEventLoadEventFired(ctx context.Context, logger *zap.Logger)...
function waitForEventNetworkIdle (line 421) | func waitForEventNetworkIdle(ctx context.Context, logger *zap.Logger) fu...
function waitForEventLoadingFinished (line 447) | func waitForEventLoadingFinished(ctx context.Context, logger *zap.Logger...
function runBatch (line 471) | func runBatch(ctx context.Context, fn ...func() error) error {
FILE: pkg/modules/chromium/events_test.go
function TestNormalizeDomain (line 5) | func TestNormalizeDomain(t *testing.T) {
function TestMatchesAnyDomain (line 31) | func TestMatchesAnyDomain(t *testing.T) {
function TestShouldCheckResourceHttpStatusCode_IgnoreDomains (line 43) | func TestShouldCheckResourceHttpStatusCode_IgnoreDomains(t *testing.T) {
function TestShouldCheckResourceHttpStatusCode_NonHTTPURL (line 59) | func TestShouldCheckResourceHttpStatusCode_NonHTTPURL(t *testing.T) {
FILE: pkg/modules/chromium/mocks.go
type ApiMock (line 12) | type ApiMock struct
method Pdf (line 17) | func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, ...
method Screenshot (line 21) | func (api *ApiMock) Screenshot(ctx context.Context, logger *zap.Logger...
type browserMock (line 26) | type browserMock struct
method pdf (line 32) | func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url...
method screenshot (line 36) | func (b *browserMock) screenshot(ctx context.Context, logger *zap.Logg...
FILE: pkg/modules/chromium/routes.go
function FormDataChromiumOptions (line 46) | func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
function FormDataChromiumPdfOptions (line 279) | func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOpt...
function FormDataChromiumScreenshotOptions (line 337) | func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData,...
function convertUrlRoute (line 405) | func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
function screenshotUrlRoute (line 451) | func screenshotUrlRoute(chromium Api) api.Route {
function convertHtmlRoute (line 480) | func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
function screenshotHtmlRoute (line 527) | func screenshotHtmlRoute(chromium Api) api.Route {
function convertMarkdownRoute (line 557) | func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api....
function screenshotMarkdownRoute (line 613) | func screenshotMarkdownRoute(chromium Api) api.Route {
function markdownToHtml (line 650) | func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []...
function convertUrl (line 725) | func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngi...
function screenshotUrl (line 857) | func screenshotUrl(ctx *api.Context, chromium Api, url string, options S...
function handleChromiumError (line 875) | func handleChromiumError(err error, options Options) error {
FILE: pkg/modules/chromium/stream.go
type streamReader (line 16) | type streamReader struct
method Read (line 25) | func (reader *streamReader) Read(p []byte) (n int, err error) {
method Close (line 75) | func (reader *streamReader) Close() error {
method next (line 84) | func (reader *streamReader) next(pos, size int) (cdprotoio.ReadReturns...
method read (line 100) | func (reader *streamReader) read(p []byte) (n int, err error) {
FILE: pkg/modules/chromium/tasks.go
function printToPdfActionFunc (line 19) | func printToPdfActionFunc(logger *zap.Logger, outputPath string, options...
function captureScreenshotActionFunc (line 116) | func captureScreenshotActionFunc(logger *zap.Logger, outputPath string, ...
function setDeviceMetricsOverride (line 165) | func setDeviceMetricsOverride(logger *zap.Logger, width, height int) chr...
function clearCacheActionFunc (line 178) | func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.Actio...
function clearCookiesActionFunc (line 197) | func clearCookiesActionFunc(logger *zap.Logger, clear bool) chromedp.Act...
function disableJavaScriptActionFunc (line 216) | func disableJavaScriptActionFunc(logger *zap.Logger, disable bool) chrom...
function setCookiesActionFunc (line 235) | func setCookiesActionFunc(logger *zap.Logger, cookies []Cookie) chromedp...
function userAgentOverride (line 284) | func userAgentOverride(logger *zap.Logger, userAgent string) chromedp.Ac...
function navigateActionFunc (line 329) | func navigateActionFunc(logger *zap.Logger, url string, skipNetworkIdleE...
function hideDefaultWhiteBackgroundActionFunc (line 363) | func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackgr...
function forceExactColorsActionFunc (line 394) | func forceExactColorsActionFunc(logger *zap.Logger, printBackground bool...
function emulateMediaTypeActionFunc (line 426) | func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string, me...
function waitDelayBeforePrintActionFunc (line 467) | func waitDelayBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip...
function waitForExpressionBeforePrintActionFunc (line 492) | func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJ...
function waitForSelectorVisibleBeforePrintActionFunc (line 534) | func waitForSelectorVisibleBeforePrintActionFunc(logger *zap.Logger, sel...
FILE: pkg/modules/exiftool/exiftool.go
function init (line 19) | func init() {
type ExifTool (line 25) | type ExifTool struct
method Descriptor (line 30) | func (engine *ExifTool) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 38) | func (engine *ExifTool) Provision(ctx *gotenberg.Context) error {
method Validate (line 50) | func (engine *ExifTool) Validate() error {
method Debug (line 60) | func (engine *ExifTool) Debug() map[string]any {
method Merge (line 77) | func (engine *ExifTool) Merge(ctx context.Context, logger *zap.Logger,...
method Split (line 82) | func (engine *ExifTool) Split(ctx context.Context, logger *zap.Logger,...
method Flatten (line 87) | func (engine *ExifTool) Flatten(ctx context.Context, logger *zap.Logge...
method Convert (line 92) | func (engine *ExifTool) Convert(ctx context.Context, logger *zap.Logge...
method ReadMetadata (line 97) | func (engine *ExifTool) ReadMetadata(ctx context.Context, logger *zap....
method WriteMetadata (line 119) | func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap...
method PageCount (line 207) | func (engine *ExifTool) PageCount(ctx context.Context, logger *zap.Log...
method WriteBookmarks (line 238) | func (engine *ExifTool) WriteBookmarks(ctx context.Context, logger *za...
method ReadBookmarks (line 243) | func (engine *ExifTool) ReadBookmarks(ctx context.Context, logger *zap...
method Encrypt (line 248) | func (engine *ExifTool) Encrypt(ctx context.Context, logger *zap.Logge...
method EmbedFiles (line 253) | func (engine *ExifTool) EmbedFiles(ctx context.Context, logger *zap.Lo...
method Watermark (line 258) | func (engine *ExifTool) Watermark(ctx context.Context, logger *zap.Log...
method Stamp (line 263) | func (engine *ExifTool) Stamp(ctx context.Context, logger *zap.Logger,...
method Rotate (line 268) | func (engine *ExifTool) Rotate(ctx context.Context, logger *zap.Logger...
FILE: pkg/modules/libreoffice/api/api.go
function init (line 22) | func init() {
type Api (line 44) | type Api struct
method Descriptor (line 235) | func (a *Api) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 252) | func (a *Api) Provision(ctx *gotenberg.Context) error {
method Validate (line 291) | func (a *Api) Validate() error {
method Start (line 309) | func (a *Api) Start() error {
method StartupMessage (line 323) | func (a *Api) StartupMessage() string {
method Stop (line 332) | func (a *Api) Stop(ctx context.Context) error {
method Debug (line 348) | func (a *Api) Debug() map[string]any {
method Metrics (line 365) | func (a *Api) Metrics() ([]gotenberg.Metric, error) {
method Checks (line 385) | func (a *Api) Checks() ([]health.CheckerOption, error) {
method Ready (line 401) | func (a *Api) Ready() error {
method LibreOffice (line 429) | func (a *Api) LibreOffice() (Uno, error) {
method Pdf (line 434) | func (a *Api) Pdf(ctx context.Context, logger *zap.Logger, inputPath, ...
method Extensions (line 454) | func (a *Api) Extensions() []string {
type Options (line 55) | type Options struct
function DefaultOptions (line 179) | func DefaultOptions() Options {
type Uno (line 218) | type Uno interface
type Provider (line 230) | type Provider interface
FILE: pkg/modules/libreoffice/api/freeport.go
function freePort (line 11) | func freePort(logger *zap.Logger) (int, error) {
FILE: pkg/modules/libreoffice/api/libreoffice.go
type libreOffice (line 19) | type libreOffice interface
type libreOfficeArguments (line 24) | type libreOfficeArguments struct
type libreOfficeProcess (line 30) | type libreOfficeProcess struct
method Start (line 51) | func (p *libreOfficeProcess) Start(logger *zap.Logger) error {
method Stop (line 182) | func (p *libreOfficeProcess) Stop(logger *zap.Logger) error {
method Healthy (line 224) | func (p *libreOfficeProcess) Healthy(logger *zap.Logger) bool {
method pdf (line 249) | func (p *libreOfficeProcess) pdf(ctx context.Context, logger *zap.Logg...
function newLibreOfficeProcess (line 41) | func newLibreOfficeProcess(arguments libreOfficeArguments) libreOffice {
FILE: pkg/modules/libreoffice/api/mocks.go
type ApiMock (line 13) | type ApiMock struct
method Pdf (line 18) | func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, input...
method Extensions (line 22) | func (api *ApiMock) Extensions() []string {
type ProviderMock (line 27) | type ProviderMock struct
method LibreOffice (line 31) | func (provider *ProviderMock) LibreOffice() (Uno, error) {
type libreOfficeMock (line 36) | type libreOfficeMock struct
method pdf (line 43) | func (b *libreOfficeMock) pdf(ctx context.Context, logger *zap.Logger,...
FILE: pkg/modules/libreoffice/libreoffice.go
function init (line 13) | func init() {
type LibreOffice (line 19) | type LibreOffice struct
method Descriptor (line 26) | func (mod *LibreOffice) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 40) | func (mod *LibreOffice) Provision(ctx *gotenberg.Context) error {
method Routes (line 72) | func (mod *LibreOffice) Routes() ([]api.Route, error) {
FILE: pkg/modules/libreoffice/pdfengine/pdfengine.go
function init (line 14) | func init() {
type LibreOfficePdfEngine (line 20) | type LibreOfficePdfEngine struct
method Descriptor (line 25) | func (engine *LibreOfficePdfEngine) Descriptor() gotenberg.ModuleDescr...
method Provision (line 33) | func (engine *LibreOfficePdfEngine) Provision(ctx *gotenberg.Context) ...
method Merge (line 50) | func (engine *LibreOfficePdfEngine) Merge(ctx context.Context, logger ...
method Split (line 55) | func (engine *LibreOfficePdfEngine) Split(ctx context.Context, logger ...
method Flatten (line 60) | func (engine *LibreOfficePdfEngine) Flatten(ctx context.Context, logge...
method Convert (line 68) | func (engine *LibreOfficePdfEngine) Convert(ctx context.Context, logge...
method ReadMetadata (line 85) | func (engine *LibreOfficePdfEngine) ReadMetadata(ctx context.Context, ...
method WriteMetadata (line 90) | func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context,...
method PageCount (line 95) | func (engine *LibreOfficePdfEngine) PageCount(ctx context.Context, log...
method WriteBookmarks (line 100) | func (engine *LibreOfficePdfEngine) WriteBookmarks(ctx context.Context...
method ReadBookmarks (line 105) | func (engine *LibreOfficePdfEngine) ReadBookmarks(ctx context.Context,...
method Encrypt (line 110) | func (engine *LibreOfficePdfEngine) Encrypt(ctx context.Context, logge...
method EmbedFiles (line 115) | func (engine *LibreOfficePdfEngine) EmbedFiles(ctx context.Context, lo...
method Watermark (line 120) | func (engine *LibreOfficePdfEngine) Watermark(ctx context.Context, log...
method Stamp (line 125) | func (engine *LibreOfficePdfEngine) Stamp(ctx context.Context, logger ...
method Rotate (line 130) | func (engine *LibreOfficePdfEngine) Rotate(ctx context.Context, logger...
FILE: pkg/modules/libreoffice/routes.go
function convertRoute (line 20) | func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEn...
FILE: pkg/modules/logging/color.go
constant black (line 12) | black color = iota + 30
constant red (line 13) | red
constant green (line 14) | green
constant yellow (line 15) | yellow
constant blue (line 16) | blue
constant magenta (line 17) | magenta
constant cyan (line 18) | cyan
constant white (line 19) | white
type color (line 22) | type color
method Add (line 24) | func (c color) Add(s string) string {
function levelToColor (line 28) | func levelToColor(l zapcore.Level) color {
FILE: pkg/modules/logging/gcp.go
function gcpSeverity (line 5) | func gcpSeverity(l zapcore.Level) string {
function gcpSeverityEncoder (line 28) | func gcpSeverityEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncod...
function gcpSeverityColorEncoder (line 32) | func gcpSeverityColorEncoder(l zapcore.Level, enc zapcore.PrimitiveArray...
FILE: pkg/modules/logging/logging.go
function init (line 17) | func init() {
constant errorLoggingLevel (line 22) | errorLoggingLevel = "error"
constant warnLoggingLevel (line 23) | warnLoggingLevel = "warn"
constant infoLoggingLevel (line 24) | infoLoggingLevel = "info"
constant debugLoggingLevel (line 25) | debugLoggingLevel = "debug"
constant autoLoggingFormat (line 29) | autoLoggingFormat = "auto"
constant jsonLoggingFormat (line 30) | jsonLoggingFormat = "json"
constant textLoggingFormat (line 31) | textLoggingFormat = "text"
type Logging (line 36) | type Logging struct
method Descriptor (line 44) | func (log *Logging) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 68) | func (log *Logging) Provision(ctx *gotenberg.Context) error {
method Validate (line 80) | func (log *Logging) Validate() error {
method Logger (line 107) | func (log *Logging) Logger(mod gotenberg.Module) (*zap.Logger, error) {
type customCore (line 129) | type customCore struct
method With (line 134) | func (c customCore) With(fields []zapcore.Field) zapcore.Core {
method Check (line 147) | func (c customCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry)...
method Write (line 158) | func (c customCore) Write(entry zapcore.Entry, fields []zapcore.Field)...
function newLogLevel (line 168) | func newLogLevel(level string) (zapcore.Level, error) {
function newLogEncoder (line 179) | func newLogEncoder(format string, gcpFields bool) (zapcore.Encoder, erro...
FILE: pkg/modules/pdfcpu/pdfcpu.go
function init (line 22) | func init() {
type PdfCpu (line 28) | type PdfCpu struct
method Descriptor (line 43) | func (engine *PdfCpu) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 51) | func (engine *PdfCpu) Provision(ctx *gotenberg.Context) error {
method Validate (line 63) | func (engine *PdfCpu) Validate() error {
method Debug (line 73) | func (engine *PdfCpu) Debug() map[string]any {
method Merge (line 99) | func (engine *PdfCpu) Merge(ctx context.Context, logger *zap.Logger, i...
method Split (line 118) | func (engine *PdfCpu) Split(ctx context.Context, logger *zap.Logger, m...
method Flatten (line 168) | func (engine *PdfCpu) Flatten(ctx context.Context, logger *zap.Logger,...
method Convert (line 173) | func (engine *PdfCpu) Convert(ctx context.Context, logger *zap.Logger,...
method ReadMetadata (line 178) | func (engine *PdfCpu) ReadMetadata(ctx context.Context, logger *zap.Lo...
method WriteMetadata (line 183) | func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.L...
method PageCount (line 188) | func (engine *PdfCpu) PageCount(ctx context.Context, logger *zap.Logge...
method ReadBookmarks (line 193) | func (engine *PdfCpu) ReadBookmarks(ctx context.Context, logger *zap.L...
method WriteBookmarks (line 272) | func (engine *PdfCpu) WriteBookmarks(ctx context.Context, logger *zap....
method EmbedFiles (line 328) | func (engine *PdfCpu) EmbedFiles(ctx context.Context, logger *zap.Logg...
method Encrypt (line 353) | func (engine *PdfCpu) Encrypt(ctx context.Context, logger *zap.Logger,...
method Watermark (line 384) | func (engine *PdfCpu) Watermark(ctx context.Context, logger *zap.Logge...
method Stamp (line 389) | func (engine *PdfCpu) Stamp(ctx context.Context, logger *zap.Logger, i...
method Rotate (line 394) | func (engine *PdfCpu) Rotate(ctx context.Context, logger *zap.Logger, ...
method applyStampOrWatermark (line 414) | func (engine *PdfCpu) applyStampOrWatermark(ctx context.Context, logge...
type pdfcpuBookmark (line 32) | type pdfcpuBookmark struct
type pdfcpuBookmarks (line 38) | type pdfcpuBookmarks struct
FILE: pkg/modules/pdfcpu/sort.go
type digitSuffixSort (line 10) | type digitSuffixSort
method Len (line 12) | func (s digitSuffixSort) Len() int {
method Swap (line 16) | func (s digitSuffixSort) Swap(i, j int) {
method Less (line 20) | func (s digitSuffixSort) Less(i, j int) bool {
function extractNumber (line 46) | func extractNumber(str string) (int, string) {
FILE: pkg/modules/pdfcpu/sort_test.go
function TestDigitSuffixSort (line 9) | func TestDigitSuffixSort(t *testing.T) {
FILE: pkg/modules/pdfengines/multi.go
type multiPdfEngines (line 14) | type multiPdfEngines struct
method Merge (line 64) | func (multi *multiPdfEngines) Merge(ctx context.Context, logger *zap.L...
method Split (line 94) | func (multi *multiPdfEngines) Split(ctx context.Context, logger *zap.L...
method Flatten (line 125) | func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *zap...
method Convert (line 150) | func (multi *multiPdfEngines) Convert(ctx context.Context, logger *zap...
method ReadMetadata (line 180) | func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger...
method WriteMetadata (line 211) | func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logge...
method PageCount (line 241) | func (multi *multiPdfEngines) PageCount(ctx context.Context, logger *z...
method ReadBookmarks (line 277) | func (multi *multiPdfEngines) ReadBookmarks(ctx context.Context, logge...
method WriteBookmarks (line 308) | func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logg...
method Encrypt (line 333) | func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *zap...
method EmbedFiles (line 358) | func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *...
method Watermark (line 383) | func (multi *multiPdfEngines) Watermark(ctx context.Context, logger *z...
method Stamp (line 408) | func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *zap.L...
method Rotate (line 433) | func (multi *multiPdfEngines) Rotate(ctx context.Context, logger *zap....
function newMultiPdfEngines (line 30) | func newMultiPdfEngines(
type splitResult (line 87) | type splitResult struct
type readMetadataResult (line 173) | type readMetadataResult struct
type pageCountResult (line 234) | type pageCountResult struct
type readBookmarksResult (line 270) | type readBookmarksResult struct
FILE: pkg/modules/pdfengines/pdfengines.go
function init (line 15) | func init() {
type PdfEngines (line 30) | type PdfEngines struct
method Descriptor (line 49) | func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 84) | func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
method Validate (line 196) | func (mod *PdfEngines) Validate() error {
method SystemMessages (line 254) | func (mod *PdfEngines) SystemMessages() []string {
method PdfEngine (line 273) | func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
method Routes (line 306) | func (mod *PdfEngines) Routes() ([]api.Route, error) {
FILE: pkg/modules/pdfengines/routes.go
function FormDataPdfSplitMode (line 19) | func FormDataPdfSplitMode(form *api.FormData, mandatory bool) gotenberg....
function FormDataPdfFormats (line 88) | func FormDataPdfFormats(form *api.FormData) gotenberg.PdfFormats {
function FormDataPdfMetadata (line 105) | func FormDataPdfMetadata(form *api.FormData, mandatory bool) map[string]...
function FormDataPdfBookmarks (line 132) | func FormDataPdfBookmarks(form *api.FormData, mandatory bool) any {
function FormDataPdfRotate (line 170) | func FormDataPdfRotate(form *api.FormData, mandatory bool) (int, string) {
function RotateStub (line 204) | func RotateStub(ctx *api.Context, engine gotenberg.PdfEngine, angle int,...
function ValidatePdfFormatsCompat (line 221) | func ValidatePdfFormatsCompat(pdfFormats gotenberg.PdfFormats, userPassw...
function MergeStub (line 256) | func MergeStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPaths ...
function SplitPdfStub (line 277) | func SplitPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, mode got...
function FlattenStub (line 330) | func FlattenStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPath...
function ConvertStub (line 344) | func ConvertStub(ctx *api.Context, engine gotenberg.PdfEngine, formats g...
function WriteMetadataStub (line 365) | func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, met...
function shiftBookmarks (line 380) | func shiftBookmarks(bookmarks []gotenberg.Bookmark, offset int) []gotenb...
function WriteBookmarksStub (line 397) | func WriteBookmarksStub(ctx *api.Context, engine gotenberg.PdfEngine, bo...
function FormDataPdfEmbeds (line 434) | func FormDataPdfEmbeds(form *api.FormData) []string {
function FormDataPdfEncrypt (line 441) | func FormDataPdfEncrypt(form *api.FormData) (userPassword, ownerPassword...
function EncryptPdfStub (line 448) | func EncryptPdfStub(ctx *api.Context, engine gotenberg.PdfEngine, userPa...
function EmbedFilesStub (line 464) | func EmbedFilesStub(ctx *api.Context, engine gotenberg.PdfEngine, embedP...
function FormDataPdfWatermark (line 481) | func FormDataPdfWatermark(form *api.FormData, mandatory bool) gotenberg....
function FormDataPdfStamp (line 486) | func FormDataPdfStamp(form *api.FormData, mandatory bool) gotenberg.Stamp {
function formDataPdfStampOrWatermark (line 490) | func formDataPdfStampOrWatermark(form *api.FormData, prefix string, mand...
function FormDataPdfWatermarkFile (line 548) | func FormDataPdfWatermarkFile(form *api.FormData) string {
function FormDataPdfStampFile (line 555) | func FormDataPdfStampFile(form *api.FormData) string {
function WatermarkStub (line 563) | func WatermarkStub(ctx *api.Context, engine gotenberg.PdfEngine, stamp g...
function StampStub (line 580) | func StampStub(ctx *api.Context, engine gotenberg.PdfEngine, stamp goten...
function mergeRoute (line 596) | func mergeRoute(engine gotenberg.PdfEngine) api.Route {
function splitRoute (line 747) | func splitRoute(engine gotenberg.PdfEngine) api.Route {
function flattenRoute (line 861) | func flattenRoute(engine gotenberg.PdfEngine) api.Route {
function convertRoute (line 896) | func convertRoute(engine gotenberg.PdfEngine) api.Route {
function readMetadataRoute (line 953) | func readMetadataRoute(engine gotenberg.PdfEngine) api.Route {
function writeMetadataRoute (line 996) | func writeMetadataRoute(engine gotenberg.PdfEngine) api.Route {
function readBookmarksRoute (line 1031) | func readBookmarksRoute(engine gotenberg.PdfEngine) api.Route {
function writeBookmarksRoute (line 1073) | func writeBookmarksRoute(engine gotenberg.PdfEngine) api.Route {
function encryptRoute (line 1108) | func encryptRoute(engine gotenberg.PdfEngine) api.Route {
function embedRoute (line 1147) | func embedRoute(engine gotenberg.PdfEngine) api.Route {
function watermarkRoute (line 1183) | func watermarkRoute(engine gotenberg.PdfEngine) api.Route {
function stampRoute (line 1234) | func stampRoute(engine gotenberg.PdfEngine) api.Route {
function rotateRoute (line 1283) | func rotateRoute(engine gotenberg.PdfEngine) api.Route {
FILE: pkg/modules/pdftk/pdftk.go
function init (line 18) | func init() {
type PdfTk (line 24) | type PdfTk struct
method Descriptor (line 29) | func (engine *PdfTk) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 37) | func (engine *PdfTk) Provision(ctx *gotenberg.Context) error {
method Validate (line 49) | func (engine *PdfTk) Validate() error {
method Debug (line 59) | func (engine *PdfTk) Debug() map[string]any {
method Split (line 82) | func (engine *PdfTk) Split(ctx context.Context, logger *zap.Logger, mo...
method Merge (line 110) | func (engine *PdfTk) Merge(ctx context.Context, logger *zap.Logger, in...
method Flatten (line 129) | func (engine *PdfTk) Flatten(ctx context.Context, logger *zap.Logger, ...
method Convert (line 134) | func (engine *PdfTk) Convert(ctx context.Context, logger *zap.Logger, ...
method ReadMetadata (line 139) | func (engine *PdfTk) ReadMetadata(ctx context.Context, logger *zap.Log...
method WriteMetadata (line 144) | func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Lo...
method PageCount (line 149) | func (engine *PdfTk) PageCount(ctx context.Context, logger *zap.Logger...
method WriteBookmarks (line 154) | func (engine *PdfTk) WriteBookmarks(ctx context.Context, logger *zap.L...
method ReadBookmarks (line 159) | func (engine *PdfTk) ReadBookmarks(ctx context.Context, logger *zap.Lo...
method Encrypt (line 164) | func (engine *PdfTk) Encrypt(ctx context.Context, logger *zap.Logger, ...
method EmbedFiles (line 202) | func (engine *PdfTk) EmbedFiles(ctx context.Context, logger *zap.Logge...
method Watermark (line 208) | func (engine *PdfTk) Watermark(ctx context.Context, logger *zap.Logger...
method Stamp (line 237) | func (engine *PdfTk) Stamp(ctx context.Context, logger *zap.Logger, in...
method Rotate (line 267) | func (engine *PdfTk) Rotate(ctx context.Context, logger *zap.Logger, i...
FILE: pkg/modules/prometheus/prometheus.go
function init (line 19) | func init() {
type Prometheus (line 25) | type Prometheus struct
method Descriptor (line 37) | func (mod *Prometheus) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 55) | func (mod *Prometheus) Provision(ctx *gotenberg.Context) error {
method Validate (line 94) | func (mod *Prometheus) Validate() error {
method Start (line 130) | func (mod *Prometheus) Start() error {
method StartupMessage (line 159) | func (mod *Prometheus) StartupMessage() string {
method Stop (line 168) | func (mod *Prometheus) Stop(ctx context.Context) error {
method Routes (line 173) | func (mod *Prometheus) Routes() ([]api.Route, error) {
FILE: pkg/modules/qpdf/qpdf.go
function init (line 18) | func init() {
type QPdf (line 24) | type QPdf struct
method Descriptor (line 30) | func (engine *QPdf) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 38) | func (engine *QPdf) Provision(ctx *gotenberg.Context) error {
method Validate (line 52) | func (engine *QPdf) Validate() error {
method Debug (line 62) | func (engine *QPdf) Debug() map[string]any {
method Split (line 85) | func (engine *QPdf) Split(ctx context.Context, logger *zap.Logger, mod...
method Merge (line 116) | func (engine *QPdf) Merge(ctx context.Context, logger *zap.Logger, inp...
method Flatten (line 139) | func (engine *QPdf) Flatten(ctx context.Context, logger *zap.Logger, i...
method Convert (line 161) | func (engine *QPdf) Convert(ctx context.Context, logger *zap.Logger, f...
method ReadMetadata (line 166) | func (engine *QPdf) ReadMetadata(ctx context.Context, logger *zap.Logg...
method WriteMetadata (line 171) | func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Log...
method PageCount (line 176) | func (engine *QPdf) PageCount(ctx context.Context, logger *zap.Logger,...
method WriteBookmarks (line 181) | func (engine *QPdf) WriteBookmarks(ctx context.Context, logger *zap.Lo...
method ReadBookmarks (line 186) | func (engine *QPdf) ReadBookmarks(ctx context.Context, logger *zap.Log...
method Encrypt (line 191) | func (engine *QPdf) Encrypt(ctx context.Context, logger *zap.Logger, i...
method EmbedFiles (line 220) | func (engine *QPdf) EmbedFiles(ctx context.Context, logger *zap.Logger...
method Watermark (line 225) | func (engine *QPdf) Watermark(ctx context.Context, logger *zap.Logger,...
method Stamp (line 230) | func (engine *QPdf) Stamp(ctx context.Context, logger *zap.Logger, inp...
method Rotate (line 235) | func (engine *QPdf) Rotate(ctx context.Context, logger *zap.Logger, in...
FILE: pkg/modules/webhook/client.go
type client (line 16) | type client struct
method send (line 29) | func (c client) send(body io.Reader, headers map[string]string, errore...
FILE: pkg/modules/webhook/middleware.go
type sendOutputFileParams (line 23) | type sendOutputFileParams struct
function webhookMiddleware (line 33) | func webhookMiddleware(w *Webhook) api.Middleware {
FILE: pkg/modules/webhook/webhook.go
function init (line 14) | func init() {
type Webhook (line 20) | type Webhook struct
method Descriptor (line 35) | func (w *Webhook) Descriptor() gotenberg.ModuleDescriptor {
method Provision (line 58) | func (w *Webhook) Provision(ctx *gotenberg.Context) error {
method Middlewares (line 76) | func (w *Webhook) Middlewares() ([]api.Middleware, error) {
method AsyncCount (line 87) | func (w *Webhook) AsyncCount() int64 {
FILE: test/integration/main_test.go
function TestMain (line 17) | func TestMain(m *testing.M) {
FILE: test/integration/scenario/compare.go
function compareJson (line 8) | func compareJson(expected, actual any) error {
FILE: test/integration/scenario/containers.go
type noopLogger (line 23) | type noopLogger struct
method Printf (line 25) | func (n *noopLogger) Printf(format string, v ...any) {
function startGotenbergContainer (line 29) | func startGotenbergContainer(ctx context.Context, env map[string]string)...
function execCommandInIntegrationToolsContainer (line 67) | func execCommandInIntegrationToolsContainer(ctx context.Context, cmd []s...
function containerHttpEndpoint (line 112) | func containerHttpEndpoint(ctx context.Context, container testcontainers...
function containerLogEntries (line 124) | func containerLogEntries(ctx context.Context, container testcontainers.C...
FILE: test/integration/scenario/http.go
function doRequest (line 13) | func doRequest(method, url string, headers map[string]string, body io.Re...
function doFormDataRequest (line 32) | func doFormDataRequest(method, url string, fields map[string]string, fil...
FILE: test/integration/scenario/scenario.go
type scenario (line 26) | type scenario struct
method reset (line 37) | func (s *scenario) reset(ctx context.Context) error {
method iHaveADefaultGotenbergContainer (line 59) | func (s *scenario) iHaveADefaultGotenbergContainer(ctx context.Context...
method iHaveAGotenbergContainerWithTheFollowingEnvironmentVariables (line 69) | func (s *scenario) iHaveAGotenbergContainerWithTheFollowingEnvironment...
method iHaveAServer (line 83) | func (s *scenario) iHaveAServer(ctx context.Context) error {
method iMakeARequestToGotenberg (line 97) | func (s *scenario) iMakeARequestToGotenberg(ctx context.Context, metho...
method iMakeARequestToGotenbergWithTheFollowingHeaders (line 101) | func (s *scenario) iMakeARequestToGotenbergWithTheFollowingHeaders(ctx...
method iMakeARequestToGotenbergWithTheFollowingFormDataAndHeaders (line 144) | func (s *scenario) iMakeARequestToGotenbergWithTheFollowingFormDataAnd...
method iMakeConcurrentRequestsToGotenberg (line 299) | func (s *scenario) iMakeConcurrentRequestsToGotenberg(ctx context.Cont...
method allConcurrentResponseStatusCodesShouldBe (line 411) | func (s *scenario) allConcurrentResponseStatusCodesShouldBe(expected i...
method allConcurrentResponsesShouldHavePdfs (line 425) | func (s *scenario) allConcurrentResponsesShouldHavePdfs(expected int) ...
method iWaitForTheAsynchronousRequestToWebhook (line 461) | func (s *scenario) iWaitForTheAsynchronousRequestToWebhook(ctx context...
method theGotenbergContainerShouldLogTheFollowingEntries (line 476) | func (s *scenario) theGotenbergContainerShouldLogTheFollowingEntries(c...
method theResponseStatusCodeShouldBe (line 519) | func (s *scenario) theResponseStatusCodeShouldBe(expected int) error {
method theHeaderValueShouldBe (line 526) | func (s *scenario) theHeaderValueShouldBe(kind, name string, expected ...
method theCookieValueShouldBe (line 544) | func (s *scenario) theCookieValueShouldBe(kind, name, expected string)...
method theBodyShouldMatchString (line 578) | func (s *scenario) theBodyShouldMatchString(kind string, expectedDoc *...
method theBodyShouldContainString (line 598) | func (s *scenario) theBodyShouldContainString(kind string, expectedDoc...
method theBodyShouldMatchJSON (line 618) | func (s *scenario) theBodyShouldMatchJSON(kind string, expectedDoc *go...
method thereShouldBePdfs (line 651) | func (s *scenario) thereShouldBePdfs(expected int, kind string) error {
method thereShouldBeTheFollowingFiles (line 680) | func (s *scenario) thereShouldBeTheFollowingFiles(kind string, filesTa...
method thePdfsShouldBeValidWithAToleranceOf (line 722) | func (s *scenario) thePdfsShouldBeValidWithAToleranceOf(ctx context.Co...
method thePdfShouldHavePages (line 792) | func (s *scenario) thePdfShouldHavePages(ctx context.Context, name str...
method thePdfShouldBeSetToLandscapeOrientation (line 848) | func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context...
method thePdfShouldHaveTheFollowingContentAtPage (line 915) | func (s *scenario) thePdfShouldHaveTheFollowingContentAtPage(ctx conte...
method thePdfsShouldBeFlatten (line 969) | func (s *scenario) thePdfsShouldBeFlatten(ctx context.Context, kind, s...
method thePdfsShouldBeEncrypted (line 1019) | func (s *scenario) thePdfsShouldBeEncrypted(ctx context.Context, kind ...
method thePdfsShouldHaveEmbeddedFile (line 1071) | func (s *scenario) thePdfsShouldHaveEmbeddedFile(ctx context.Context, ...
function InitializeScenario (line 1125) | func InitializeScenario(ctx *godog.ScenarioContext) {
FILE: test/integration/scenario/server.go
type server (line 21) | type server struct
method start (line 173) | func (s *server) start(ctx context.Context) (int, error) {
method stop (line 193) | func (s *server) stop(ctx context.Context) error {
function newServer (line 28) | func newServer(ctx context.Context, workdir string) (*server, error) {
Condensed preview — 274 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,324K chars).
[
{
"path": ".bruno/Chromium/Convert/HTML to PDF.bru",
"chars": 1888,
"preview": "meta {\n name: HTML to PDF\n type: http\n seq: 2\n}\n\npost {\n url: {{baseUrl}}/forms/chromium/convert/html\n body: multip"
},
{
"path": ".bruno/Chromium/Convert/Markdown to PDF.bru",
"chars": 1974,
"preview": "meta {\n name: Markdown to PDF\n type: http\n seq: 3\n}\n\npost {\n url: {{baseUrl}}/forms/chromium/convert/markdown\n body"
},
{
"path": ".bruno/Chromium/Convert/URL to PDF.bru",
"chars": 1842,
"preview": "meta {\n name: URL to PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/chromium/convert/url\n body: multipar"
},
{
"path": ".bruno/Chromium/Screenshot/HTML Screenshot.bru",
"chars": 1212,
"preview": "meta {\n name: HTML Screenshot\n type: http\n seq: 2\n}\n\npost {\n url: {{baseUrl}}/forms/chromium/screenshot/html\n body:"
},
{
"path": ".bruno/Chromium/Screenshot/Markdown Screenshot.bru",
"chars": 1298,
"preview": "meta {\n name: Markdown Screenshot\n type: http\n seq: 3\n}\n\npost {\n url: {{baseUrl}}/forms/chromium/screenshot/markdown"
},
{
"path": ".bruno/Chromium/Screenshot/URL Screenshot.bru",
"chars": 1166,
"preview": "meta {\n name: URL Screenshot\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/chromium/screenshot/url\n body: m"
},
{
"path": ".bruno/Health & Info/Debug.bru",
"chars": 105,
"preview": "meta {\n name: Debug\n type: http\n seq: 3\n}\n\nget {\n url: {{baseUrl}}/debug\n body: none\n auth: none\n}\n"
},
{
"path": ".bruno/Health & Info/Health.bru",
"chars": 107,
"preview": "meta {\n name: Health\n type: http\n seq: 1\n}\n\nget {\n url: {{baseUrl}}/health\n body: none\n auth: none\n}\n"
},
{
"path": ".bruno/Health & Info/Prometheus Metrics.bru",
"chars": 131,
"preview": "meta {\n name: Prometheus Metrics\n type: http\n seq: 4\n}\n\nget {\n url: {{baseUrl}}/prometheus/metrics\n body: none\n au"
},
{
"path": ".bruno/Health & Info/Version.bru",
"chars": 109,
"preview": "meta {\n name: Version\n type: http\n seq: 2\n}\n\nget {\n url: {{baseUrl}}/version\n body: none\n auth: none\n}\n"
},
{
"path": ".bruno/LibreOffice/Convert to PDF.bru",
"chars": 1894,
"preview": "meta {\n name: Convert to PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/libreoffice/convert\n body: multi"
},
{
"path": ".bruno/PDF Engines/Bookmarks/Read Bookmarks.bru",
"chars": 507,
"preview": "meta {\n name: Read Bookmarks\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/bookmarks/read\n body:"
},
{
"path": ".bruno/PDF Engines/Bookmarks/Write Bookmarks.bru",
"chars": 646,
"preview": "meta {\n name: Write Bookmarks\n type: http\n seq: 2\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/bookmarks/write\n bod"
},
{
"path": ".bruno/PDF Engines/Convert/Convert PDF.bru",
"chars": 569,
"preview": "meta {\n name: Convert PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/convert\n body: multipart"
},
{
"path": ".bruno/PDF Engines/Embed/Embed Files.bru",
"chars": 677,
"preview": "meta {\n name: Embed Files\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/embed\n body: multipartFo"
},
{
"path": ".bruno/PDF Engines/Encrypt/Encrypt PDF.bru",
"chars": 590,
"preview": "meta {\n name: Encrypt PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/encrypt\n body: multipart"
},
{
"path": ".bruno/PDF Engines/Flatten/Flatten PDF.bru",
"chars": 537,
"preview": "meta {\n name: Flatten PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/flatten\n body: multipart"
},
{
"path": ".bruno/PDF Engines/Merge/Merge PDFs.bru",
"chars": 1124,
"preview": "meta {\n name: Merge PDFs\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/merge\n body: multipartFor"
},
{
"path": ".bruno/PDF Engines/Metadata/Read Metadata.bru",
"chars": 505,
"preview": "meta {\n name: Read Metadata\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/metadata/read\n body: m"
},
{
"path": ".bruno/PDF Engines/Metadata/Write Metadata.bru",
"chars": 742,
"preview": "meta {\n name: Write Metadata\n type: http\n seq: 2\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/metadata/write\n body:"
},
{
"path": ".bruno/PDF Engines/Rotate/Rotate PDF.bru",
"chars": 571,
"preview": "meta {\n name: Rotate PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/rotate\n body: multipartFo"
},
{
"path": ".bruno/PDF Engines/Split/Split PDF.bru",
"chars": 1022,
"preview": "meta {\n name: Split PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/split\n body: multipartForm"
},
{
"path": ".bruno/PDF Engines/Stamp/Stamp PDF.bru",
"chars": 839,
"preview": "meta {\n name: Stamp PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/stamp\n body: multipartForm"
},
{
"path": ".bruno/PDF Engines/Watermark/Watermark PDF.bru",
"chars": 884,
"preview": "meta {\n name: Watermark PDF\n type: http\n seq: 1\n}\n\npost {\n url: {{baseUrl}}/forms/pdfengines/watermark\n body: multi"
},
{
"path": ".bruno/bruno.json",
"chars": 106,
"preview": "{\n \"version\": \"1\",\n \"name\": \"Gotenberg\",\n \"type\": \"collection\",\n \"ignore\": [\"node_modules\", \".git\"]\n}\n"
},
{
"path": ".bruno/collection.bru",
"chars": 43,
"preview": "headers {\n Gotenberg-Trace: {{traceId}}\n}\n"
},
{
"path": ".bruno/environments/Demo.bru",
"chars": 69,
"preview": "vars {\n baseUrl: https://demo.gotenberg.dev\n traceId: bruno-demo\n}\n"
},
{
"path": ".bruno/environments/Local.bru",
"chars": 65,
"preview": "vars {\n baseUrl: http://localhost:3000\n traceId: bruno-local\n}\n"
},
{
"path": ".dockerignore",
"chars": 5,
"preview": ".git\n"
},
{
"path": ".github/FUNDING.yml",
"chars": 17,
"preview": "github: [gulien]\n"
},
{
"path": ".github/actions/build-test-push/action.yml",
"chars": 3449,
"preview": "name: Build Test Push\ndescription: Build, test and push Docker images for a given platform\nauthor: Julien Neuhart\n\ninput"
},
{
"path": ".github/actions/build-test-push/build.sh",
"chars": 4701,
"preview": "#!/bin/bash\n\n# Exit early.\n# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.\nset "
},
{
"path": ".github/actions/build-test-push/push.sh",
"chars": 1114,
"preview": "#!/bin/bash\n\n# Exit early.\n# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.\nset "
},
{
"path": ".github/actions/build-test-push/test.sh",
"chars": 1352,
"preview": "#!/bin/bash\n\n# Exit early.\n# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.\nset "
},
{
"path": ".github/actions/clean/action.yml",
"chars": 976,
"preview": "name: Clean\ndescription: Clean tags from Docker Hub\nauthor: Julien Neuhart\n\ninputs:\n docker_hub_username:\n descripti"
},
{
"path": ".github/actions/clean/clean.sh",
"chars": 2521,
"preview": "#!/bin/bash\n\n# Exit early.\n# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.\nset "
},
{
"path": ".github/actions/merge/action.yml",
"chars": 1378,
"preview": "name: Merge\ndescription: Merge tags to single multi-platform tags\nauthor: Julien Neuhart\n\ninputs:\n github_token:\n de"
},
{
"path": ".github/actions/merge/merge.sh",
"chars": 1935,
"preview": "#!/bin/bash\n\n# Exit early.\n# See: https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#The-Set-Builtin.\nset "
},
{
"path": ".github/dependabot.yml",
"chars": 389,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n "
},
{
"path": ".github/stale.yml",
"chars": 701,
"preview": "# Number of days of inactivity before an issue becomes stale\ndaysUntilStale: 7\n# Number of days of inactivity before a s"
},
{
"path": ".github/workflows/continuous-delivery.yml",
"chars": 5415,
"preview": "name: Continuous Delivery\n\non:\n release:\n types: [published]\n\npermissions:\n contents: read\n\njobs:\n release_amd64:\n"
},
{
"path": ".github/workflows/continuous-integration.yml",
"chars": 13321,
"preview": "name: Continuous Integration\n\non:\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n\nconcurr"
},
{
"path": ".github/workflows/pull-request-cleanup.yml",
"chars": 551,
"preview": "name: Pull Request Cleanup\n\non:\n pull_request:\n types: [closed]\n\npermissions:\n contents: read\n\njobs:\n cleanup:\n "
},
{
"path": ".gitignore",
"chars": 39,
"preview": ".idea\n.vscode\n.DS_Store\n/node_modules/\n"
},
{
"path": ".golangci.yml",
"chars": 1062,
"preview": "version: \"2\"\nrun:\n issues-exit-code: 1\n tests: false\nlinters:\n default: none\n enable:\n - asasalint\n - asciiche"
},
{
"path": ".node-version",
"chars": 8,
"preview": "24.11.0\n"
},
{
"path": ".prettierignore",
"chars": 6,
"preview": "build\n"
},
{
"path": ".prettierrc",
"chars": 96,
"preview": "{\n \"plugins\": [\"prettier-plugin-gherkin\", \"prettier-plugin-sh\"],\n \"escapeBackslashes\": true\n}\n"
},
{
"path": "AGENTS.md",
"chars": 17460,
"preview": "# Operational Guidelines for Gotenberg\n\nYou are working on **Gotenberg**, a Docker-based API for converting documents to"
},
{
"path": "CLAUDE.md",
"chars": 282,
"preview": "# Claude Code — Gotenberg\n\nRead [AGENTS.md](AGENTS.md) first. It contains everything: core principles, project layout, c"
},
{
"path": "CONTRIBUTING.md",
"chars": 2370,
"preview": "# Contributing to Gotenberg\n\nThank you for your interest in contributing to Gotenberg! This guide will help you get star"
},
{
"path": "GEMINI.md",
"chars": 277,
"preview": "# Gemini — Gotenberg\n\nRead [AGENTS.md](AGENTS.md) first. It contains everything: core principles, project layout, coding"
},
{
"path": "LICENSE",
"chars": 1065,
"preview": "MIT License\n\nCopyright (c) Julien Neuhart\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "Makefile",
"chars": 8903,
"preview": "include .env\n\n.PHONY: help\nhelp: ## Show the help\n\t@grep -hE '^[A-Za-z0-9_ \\-]*?:.*##.*$$' $(MAKEFILE_LIST) | sort | awk"
},
{
"path": "README.md",
"chars": 2715,
"preview": "<p align=\"center\">\n <img src=\"https://user-images.githubusercontent.com/8983173/130322857-185831e2-f041-46eb-a17f-0a6"
},
{
"path": "SECURITY.md",
"chars": 1711,
"preview": "# Security Policy\n\n## Supported Versions\n\nPlease ensure to keep your environment up to date and use only the latest vers"
},
{
"path": "build/Dockerfile",
"chars": 10853,
"preview": "# ARG instructions do not create additional layers. Instead, next layers will\n# concatenate them. Also, we have to repea"
},
{
"path": "build/Dockerfile.aws-lambda",
"chars": 484,
"preview": "ARG DOCKER_REGISTRY\nARG DOCKER_REPOSITORY\nARG GOTENBERG_VERSION\n\nFROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VER"
},
{
"path": "build/Dockerfile.cloudrun",
"chars": 669,
"preview": "ARG DOCKER_REGISTRY\nARG DOCKER_REPOSITORY\nARG GOTENBERG_VERSION\n\nFROM $DOCKER_REGISTRY/$DOCKER_REPOSITORY:$GOTENBERG_VER"
},
{
"path": "build/chromium-hyphen-data/120.0.6050.0/_metadata/verified_contents.json",
"chars": 7328,
"preview": "[{\"description\":\"treehash per file\",\"signed_content\":{\"payload\":\"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGl"
},
{
"path": "build/chromium-hyphen-data/120.0.6050.0/manifest.json",
"chars": 82,
"preview": "{\n \"manifest_version\": 2,\n \"name\": \"hyphens-data\",\n \"version\": \"120.0.6050.0\"\n}"
},
{
"path": "build/fonts.conf",
"chars": 628,
"preview": "<?xml version='1.0'?>\n<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>\n<fontconfig>\n <match target=\"font\">\n <edit mode=\"assign\""
},
{
"path": "cmd/gotenberg/main.go",
"chars": 191,
"preview": "package main\n\nimport (\n\tgotenbergcmd \"github.com/gotenberg/gotenberg/v8/cmd\"\n\t// Gotenberg modules.\n\t_ \"github.com/goten"
},
{
"path": "cmd/gotenberg.go",
"chars": 5162,
"preview": "package gotenbergcmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\tflag \"gith"
},
{
"path": "go.mod",
"chars": 5750,
"preview": "module github.com/gotenberg/gotenberg/v8\n\ngo 1.26.0\n\nrequire (\n\tgithub.com/alexliesenfeld/health v0.8.1\n\tgithub.com/bara"
},
{
"path": "go.sum",
"chars": 29631,
"preview": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMI"
},
{
"path": "package.json",
"chars": 133,
"preview": "{\n \"devDependencies\": {\n \"prettier\": \"3.8.1\",\n \"prettier-plugin-gherkin\": \"^3.1.3\",\n \"prettier-plugin-sh\": \"^0"
},
{
"path": "pkg/gotenberg/cmd.go",
"chars": 5454,
"preview": "package gotenberg\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"go.uber.org/z"
},
{
"path": "pkg/gotenberg/context.go",
"chars": 3245,
"preview": "package gotenberg\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\n// Context is a struct that helps to initialize modules. When provision"
},
{
"path": "pkg/gotenberg/context_test.go",
"chars": 5445,
"preview": "package gotenberg\n\nimport (\n\t\"errors\"\n\t\"testing\"\n)\n\nfunc TestContext_Module(t *testing.T) {\n\tfor _, tc := range []struct"
},
{
"path": "pkg/gotenberg/debug.go",
"chars": 1592,
"preview": "package gotenberg\n\nimport (\n\t\"runtime\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tflag \"github.com/spf13/pflag\"\n)\n\n// DebugInfo gathers d"
},
{
"path": "pkg/gotenberg/debug_test.go",
"chars": 1517,
"preview": "package gotenberg\n\nimport (\n\t\"reflect\"\n\t\"runtime\"\n\t\"testing\"\n\n\tflag \"github.com/spf13/pflag\"\n)\n\nfunc TestBuildDebug(t *t"
},
{
"path": "pkg/gotenberg/doc.go",
"chars": 264,
"preview": "// Package gotenberg provides most of the logic of the module system.\n//\n// caddyserver/caddy, licensed under the Apache"
},
{
"path": "pkg/gotenberg/env.go",
"chars": 843,
"preview": "package gotenberg\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n)\n\n// StringEnv retrieves the value of the environment variable name"
},
{
"path": "pkg/gotenberg/env_test.go",
"chars": 2686,
"preview": "package gotenberg\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestStringEnv(t *testing.T) {\n\tfor _, tc := range []struct {\n\t\tscen"
},
{
"path": "pkg/gotenberg/filter.go",
"chars": 1485,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/dlclark/regexp2\"\n)\n\n// ErrFiltered happens"
},
{
"path": "pkg/gotenberg/filter_test.go",
"chars": 2246,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/dlclark/regexp2\"\n)\n\nfunc TestFilterDea"
},
{
"path": "pkg/gotenberg/flags.go",
"chars": 5852,
"preview": "package gotenberg\n\nimport (\n\t\"time\"\n\n\t\"github.com/dlclark/regexp2\"\n\t\"github.com/labstack/gommon/bytes\"\n\tflag \"github.com"
},
{
"path": "pkg/gotenberg/flags_test.go",
"chars": 18313,
"preview": "package gotenberg\n\nimport (\n\t\"reflect\"\n\t\"regexp\"\n\t\"testing\"\n\t\"time\"\n\n\tflag \"github.com/spf13/pflag\"\n)\n\nfunc TestParsedFl"
},
{
"path": "pkg/gotenberg/fs.go",
"chars": 2679,
"preview": "package gotenberg\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/google/uuid\"\n)\n\n// MkdirAll defines the method signature for crea"
},
{
"path": "pkg/gotenberg/gc.go",
"chars": 1269,
"preview": "package gotenberg\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n)\n\n// GarbageCollect sc"
},
{
"path": "pkg/gotenberg/gc_test.go",
"chars": 2382,
"preview": "package gotenberg\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"go.uber.org/zap\"\n)\n\nfun"
},
{
"path": "pkg/gotenberg/logging.go",
"chars": 1759,
"preview": "package gotenberg\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-retryablehttp\"\n\t\"go.uber.org/zap\"\n)\n\n// LoggerProvider is "
},
{
"path": "pkg/gotenberg/metrics.go",
"chars": 631,
"preview": "package gotenberg\n\n// Metric represents a unitary metric.\ntype Metric struct {\n\t// Name is the unique identifier.\n\t// Re"
},
{
"path": "pkg/gotenberg/mocks.go",
"chars": 8358,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"go.uber.org/zap\"\n)\n\n// ModuleMock is a mock for the [Module] interface.\n"
},
{
"path": "pkg/gotenberg/modules.go",
"chars": 3443,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"sync\"\n\n\tflag \"github.com/spf13/pflag\"\n)\n\n// Module is a sort of "
},
{
"path": "pkg/gotenberg/modules_test.go",
"chars": 2131,
"preview": "package gotenberg\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestMustRegisterModule(t *testing.T) {\n\tdescriptorsMu.RLock()\n"
},
{
"path": "pkg/gotenberg/pdfengine.go",
"chars": 8290,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"go.uber.org/zap\"\n)\n\nvar (\n\t// ErrPdfEngineMethodNotSupported "
},
{
"path": "pkg/gotenberg/shutdown.go",
"chars": 292,
"preview": "package gotenberg\n\nimport \"errors\"\n\n// ErrCancelGracefulShutdownContext tells that a module wants to abort a\n// graceful"
},
{
"path": "pkg/gotenberg/sort.go",
"chars": 2780,
"preview": "package gotenberg\n\nimport (\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n)\n\ntype numberLoc int\n\nconst (\n\tnumberNone num"
},
{
"path": "pkg/gotenberg/sort_test.go",
"chars": 1970,
"preview": "package gotenberg\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n)\n\nfunc TestAlphanumericSort(t *testing.T) {\n\tfor _, tc := ran"
},
{
"path": "pkg/gotenberg/supervisor.go",
"chars": 9645,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"go.uber.org/zap\"\n)\n\n// ErrProcessAlrea"
},
{
"path": "pkg/gotenberg/supervisor_test.go",
"chars": 17413,
"preview": "package gotenberg\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.uber.org/zap\"\n)\n\nfunc T"
},
{
"path": "pkg/gotenberg/version.go",
"chars": 103,
"preview": "package gotenberg\n\n// Version is the... version of the Gotenberg application.\nvar Version = \"snapshot\"\n"
},
{
"path": "pkg/modules/api/api.go",
"chars": 18970,
"preview": "package api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/alexliesen"
},
{
"path": "pkg/modules/api/context.go",
"chars": 16539,
"preview": "package api\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"p"
},
{
"path": "pkg/modules/api/context_test.go",
"chars": 1847,
"preview": "package api\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"githu"
},
{
"path": "pkg/modules/api/doc.go",
"chars": 156,
"preview": "// Package api provides a module, which is an HTTP server. Other modules may\n// add multipart/form-data routes, middlewa"
},
{
"path": "pkg/modules/api/errors.go",
"chars": 2046,
"preview": "package api\n\n// Credits: https://www.joeshaw.org/error-handling-in-go-http-applications.\n\n// HttpError is an interface a"
},
{
"path": "pkg/modules/api/errors_test.go",
"chars": 2344,
"preview": "package api\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestNewSentinelHttpError(t *testing.T) {\n\tactu"
},
{
"path": "pkg/modules/api/formdata.go",
"chars": 16000,
"preview": "package api\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n"
},
{
"path": "pkg/modules/api/formdata_test.go",
"chars": 36356,
"preview": "package api\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestFormData_Validate(t *t"
},
{
"path": "pkg/modules/api/middlewares.go",
"chars": 11974,
"preview": "package api\n\nimport (\n\t\"context\"\n\t\"crypto/subtle\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gi"
},
{
"path": "pkg/modules/api/mocks.go",
"chars": 3517,
"preview": "package api\n\nimport (\n\t\"github.com/alexliesenfeld/health\"\n\t\"github.com/labstack/echo/v4\"\n\t\"go.uber.org/zap\"\n\n\t\"github.co"
},
{
"path": "pkg/modules/api/testdata/sample.txt",
"chars": 32,
"preview": "This is a text from a text file."
},
{
"path": "pkg/modules/chromium/browser.go",
"chars": 16125,
"preview": "package chromium\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tcdprotobrowser "
},
{
"path": "pkg/modules/chromium/chromium.go",
"chars": 22877,
"preview": "package chromium\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alex"
},
{
"path": "pkg/modules/chromium/debug.go",
"chars": 569,
"preview": "package chromium\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"go.uber.org/zap\"\n)\n\n// debugLogger is wrapper around a [zap.Logger] which is "
},
{
"path": "pkg/modules/chromium/doc.go",
"chars": 184,
"preview": "// Package chromium provides a module which adds routes for converting HTML\n// documents to PDF. Other modules may also "
},
{
"path": "pkg/modules/chromium/events.go",
"chars": 14243,
"preview": "package chromium\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/chromedp/"
},
{
"path": "pkg/modules/chromium/events_test.go",
"chars": 2064,
"preview": "package chromium\n\nimport \"testing\"\n\nfunc TestNormalizeDomain(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\tin s"
},
{
"path": "pkg/modules/chromium/mocks.go",
"chars": 1601,
"preview": "package chromium\n\nimport (\n\t\"context\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/gotenberg/gotenberg/v8/pkg/gotenberg\"\n)\n\n// ApiM"
},
{
"path": "pkg/modules/chromium/routes.go",
"chars": 30143,
"preview": "package chromium\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"html/template\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath"
},
{
"path": "pkg/modules/chromium/stream.go",
"chars": 2398,
"preview": "package chromium\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/chromedp/cdproto"
},
{
"path": "pkg/modules/chromium/tasks.go",
"chars": 14658,
"preview": "package chromium\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/chromedp/cdproto/cdp\"\n\t\"gith"
},
{
"path": "pkg/modules/exiftool/doc.go",
"chars": 374,
"preview": "// Package exiftool provides an implementation of the gotenberg.PdfEngine\n// interface using the ExifTool command-line t"
},
{
"path": "pkg/modules/exiftool/exiftool.go",
"chars": 9715,
"preview": "package exiftool\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"reflect\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/b"
},
{
"path": "pkg/modules/libreoffice/api/api.go",
"chars": 15989,
"preview": "package api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alexliese"
},
{
"path": "pkg/modules/libreoffice/api/doc.go",
"chars": 152,
"preview": "// Package api provides a module which manages a LibreOffice instance and\n// interacts with it via the UNO (Universal Ne"
},
{
"path": "pkg/modules/libreoffice/api/freeport.go",
"chars": 603,
"preview": "package api\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\n\t\"go.uber.org/zap\"\n)\n\nfunc freePort(logger *zap.Logger) (int, error) {\n\t"
},
{
"path": "pkg/modules/libreoffice/api/libreoffice.go",
"chars": 11750,
"preview": "package api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"go.uber.org/"
},
{
"path": "pkg/modules/libreoffice/api/mocks.go",
"chars": 1530,
"preview": "package api\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/gotenberg/gotenberg/v8/pkg/gotenberg\"\n)\n\n//"
},
{
"path": "pkg/modules/libreoffice/doc.go",
"chars": 133,
"preview": "// Package libreoffice provides a module which adds a route for converting\n// documents to PDF with LibreOffice.\npackage"
},
{
"path": "pkg/modules/libreoffice/libreoffice.go",
"chars": 2172,
"preview": "package libreoffice\n\nimport (\n\t\"fmt\"\n\n\tflag \"github.com/spf13/pflag\"\n\n\t\"github.com/gotenberg/gotenberg/v8/pkg/gotenberg\""
},
{
"path": "pkg/modules/libreoffice/pdfengine/doc.go",
"chars": 264,
"preview": "// Package pdfengine provides a module which interacts with LibreOffice via the\n// UNO (Universal Network Objects) API a"
},
{
"path": "pkg/modules/libreoffice/pdfengine/pdfengine.go",
"chars": 5978,
"preview": "package pdfengine\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"go.uber.org/zap\"\n\n\t\"github.com/gotenberg/gotenberg/v8/pkg/got"
},
{
"path": "pkg/modules/libreoffice/routes.go",
"chars": 14685,
"preview": "package libreoffice\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/labstack/echo/v4\"\n\n\t\"githu"
},
{
"path": "pkg/modules/logging/color.go",
"chars": 744,
"preview": "package logging\n\nimport (\n\t\"fmt\"\n\n\t\"go.uber.org/zap/zapcore\"\n)\n\n// Foreground colors.\n// Copy pasted from go.uber.org/za"
},
{
"path": "pkg/modules/logging/doc.go",
"chars": 111,
"preview": "// Package logging provides a module which creates a zap.Logger instance for\n// other modules.\npackage logging\n"
},
{
"path": "pkg/modules/logging/gcp.go",
"chars": 781,
"preview": "package logging\n\nimport \"go.uber.org/zap/zapcore\"\n\nfunc gcpSeverity(l zapcore.Level) string {\n\tswitch l {\n\tcase zapcore."
},
{
"path": "pkg/modules/logging/logging.go",
"chars": 6664,
"preview": "package logging\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\tflag \"github.com/spf13/pflag\"\n\t\"go.uber.org/multierr\"\n\t\"go.uber.org/zap"
},
{
"path": "pkg/modules/pdfcpu/doc.go",
"chars": 275,
"preview": "// Package pdfcpu provides an implementation of the gotenberg.PdfEngine\n// interface using the pdfcpu command-line tool."
},
{
"path": "pkg/modules/pdfcpu/pdfcpu.go",
"chars": 13612,
"preview": "package pdfcpu\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sort\""
},
{
"path": "pkg/modules/pdfcpu/sort.go",
"chars": 1499,
"preview": "package pdfcpu\n\nimport (\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n)\n\ntype digitSuffixSort []string\n\nfunc (s digitSu"
},
{
"path": "pkg/modules/pdfcpu/sort_test.go",
"chars": 914,
"preview": "package pdfcpu\n\nimport (\n\t\"reflect\"\n\t\"sort\"\n\t\"testing\"\n)\n\nfunc TestDigitSuffixSort(t *testing.T) {\n\tfor _, tc := range ["
},
{
"path": "pkg/modules/pdfengines/doc.go",
"chars": 138,
"preview": "// Package pdfengines a way to gather and manage multiple modules that\n// implement the gotenberg.PdfEngine interface.\np"
},
{
"path": "pkg/modules/pdfengines/multi.go",
"chars": 12941,
"preview": "package pdfengines\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"go.uber.org/multierr\"\n\t\"go.uber.org/zap\"\n\n\t\"github.com/gotenbe"
},
{
"path": "pkg/modules/pdfengines/pdfengines.go",
"chars": 12384,
"preview": "package pdfengines\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\tflag \"github.com/spf13/pflag\"\n\n\t\"github.com/gotenbe"
},
{
"path": "pkg/modules/pdfengines/routes.go",
"chars": 35831,
"preview": "package pdfengines\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"git"
},
{
"path": "pkg/modules/pdftk/doc.go",
"chars": 378,
"preview": "// Package pdftk provides an implementation of the gotenberg.PdfEngine\n// interface using the PDFtk command-line tool. T"
},
{
"path": "pkg/modules/pdftk/pdftk.go",
"chars": 9824,
"preview": "package pdftk\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"syscall\"\n\n\t\"go.uber.org"
},
{
"path": "pkg/modules/prometheus/doc.go",
"chars": 156,
"preview": "// Package prometheus provides a module which collects metrics and exposes them\n// via an HTTP route.\n//\n// See: https:/"
},
{
"path": "pkg/modules/prometheus/prometheus.go",
"chars": 4958,
"preview": "package prometheus\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/labstack/echo/v4\"\n\t\"github.co"
},
{
"path": "pkg/modules/qpdf/doc.go",
"chars": 396,
"preview": "// Package qpdf provides an implementation of the gotenberg.PdfEngine\n// interface using the QPDF command-line tool. Thi"
},
{
"path": "pkg/modules/qpdf/qpdf.go",
"chars": 7981,
"preview": "package qpdf\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"syscall\"\n\n\t\"go.uber.org/"
},
{
"path": "pkg/modules/webhook/client.go",
"chars": 2927,
"preview": "package webhook\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-retryablehttp\"\n\t\"github"
},
{
"path": "pkg/modules/webhook/doc.go",
"chars": 154,
"preview": "// Package webhook provides a module which adds a middleware for uploading\n// output files to any destination in an asyn"
},
{
"path": "pkg/modules/webhook/middleware.go",
"chars": 11072,
"preview": "package webhook\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\""
},
{
"path": "pkg/modules/webhook/webhook.go",
"chars": 3550,
"preview": "package webhook\n\nimport (\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/dlclark/regexp2\"\n\tflag \"github.com/spf13/pflag\"\n\n\t\"github"
},
{
"path": "pkg/standard/doc.go",
"chars": 80,
"preview": "// Package standard imports the application's default modules.\npackage standard\n"
},
{
"path": "pkg/standard/imports.go",
"chars": 852,
"preview": "package standard\n\nimport (\n\t// Standard Gotenberg modules.\n\t_ \"github.com/gotenberg/gotenberg/v8/pkg/modules/api\"\n\t_ \"gi"
},
{
"path": "test/integration/doc.go",
"chars": 95,
"preview": "// Package integration contains everything related to integration testing.\npackage integration\n"
},
{
"path": "test/integration/features/chromium_concurrent.feature",
"chars": 1135,
"preview": "@chromium\n@chromium-concurrent\nFeature: Chromium concurrent conversions\n\n Scenario: Concurrent HTML to PDF conversions "
},
{
"path": "test/integration/features/chromium_convert_html.feature",
"chars": 67286,
"preview": "# TODO:\n# 1. JavaScript disabled on some feature.\n\n@chromium\n@chromium-convert-html\nFeature: /forms/chromium/convert/htm"
},
{
"path": "test/integration/features/chromium_convert_markdown.feature",
"chars": 70358,
"preview": "# TODO:\n# 1. JavaScript disabled on some feature.\n\n@chromium\n@chromium-convert-markdown\nFeature: /forms/chromium/convert"
},
{
"path": "test/integration/features/chromium_convert_url.feature",
"chars": 76697,
"preview": "# TODO:\n# 1. JavaScript disabled on some feature.\n\n@chromium\n@chromium-convert-url\nFeature: /forms/chromium/convert/url\n"
},
{
"path": "test/integration/features/debug.feature",
"chars": 11174,
"preview": "@debug\nFeature: /debug\n\n Scenario: GET /debug (Disabled)\n Given I have a default Gotenberg container\n When I make"
},
{
"path": "test/integration/features/health.feature",
"chars": 4314,
"preview": "# TODO:\n# 1. Check if down for each module.\n# 2. Restarting modules do not make health check fail.\n\n@health\nFeature: /he"
},
{
"path": "test/integration/features/libreoffice_convert.feature",
"chars": 46767,
"preview": "@libreoffice\n@libreoffice-convert\nFeature: /forms/libreoffice/convert\n\n Scenario: POST /forms/libreoffice/convert (Sing"
},
{
"path": "test/integration/features/output_filename.feature",
"chars": 1833,
"preview": "@output-filename\nFeature: Output Filename\n\n Scenario: Default (Single Output File)\n Given I have a default Gotenberg"
},
{
"path": "test/integration/features/pdfengines_bookmarks.feature",
"chars": 18099,
"preview": "@pdfengines\n@pdfengines-bookmarks\n@bookmarks\nFeature: /forms/pdfengines/bookmarks/{write|read}\n\n Scenario: POST /forms/"
},
{
"path": "test/integration/features/pdfengines_convert.feature",
"chars": 11074,
"preview": "# TODO:\n# 1. PDF/UA-2.\n\n@pdfengines\n@pdfengines-convert\nFeature: /forms/pdfengines/convert\n\n Scenario: POST /forms/pdfe"
},
{
"path": "test/integration/features/pdfengines_embed.feature",
"chars": 4328,
"preview": "@pdfengines\n@pdfengines-embed\n@embed\nFeature: /forms/pdfengines/embed\n\n Scenario: POST /forms/pdfengines/embed\n Give"
},
{
"path": "test/integration/features/pdfengines_encrypt.feature",
"chars": 10668,
"preview": "@pdfengines\n@pdfengines-encrypt\n@encrypt\nFeature: /forms/pdfengines/encrypt\n\n Scenario: POST /forms/pdfengines/encrypt "
},
{
"path": "test/integration/features/pdfengines_flatten.feature",
"chars": 6660,
"preview": "@pdfengines\n@pdfengines-flatten\n@flatten\nFeature: /forms/pdfengines/flatten\n\n Scenario: POST /forms/pdfengines/flatten "
},
{
"path": "test/integration/features/pdfengines_merge.feature",
"chars": 38814,
"preview": "@pdfengines\n@pdfengines-merge\n@merge\nFeature: /forms/pdfengines/merge\n\n Scenario: POST /forms/pdfengines/merge (default"
},
{
"path": "test/integration/features/pdfengines_metadata.feature",
"chars": 24021,
"preview": "@pdfengines\n@pdfengines-metadata\n@metadata\nFeature: /forms/pdfengines/metadata/{write|read}\n\n Scenario: POST /forms/pdf"
},
{
"path": "test/integration/features/pdfengines_rotate.feature",
"chars": 9271,
"preview": "@pdfengines\n@pdfengines-rotate\n@rotate\nFeature: /forms/pdfengines/rotate\n\n Scenario: POST /forms/pdfengines/rotate (90 "
},
{
"path": "test/integration/features/pdfengines_split.feature",
"chars": 41392,
"preview": "@pdfengines\n@pdfengines-split\n@split\nFeature: /forms/pdfengines/split\n\n Scenario: POST /forms/pdfengines/split (Interva"
},
{
"path": "test/integration/features/pdfengines_stamp.feature",
"chars": 13782,
"preview": "@pdfengines\n@pdfengines-stamp\n@stamp\nFeature: /forms/pdfengines/stamp\n\n Scenario: POST /forms/pdfengines/stamp (Text - "
},
{
"path": "test/integration/features/pdfengines_watermark.feature",
"chars": 14264,
"preview": "@pdfengines\n@pdfengines-watermark\n@watermark\nFeature: /forms/pdfengines/watermark\n\n Scenario: POST /forms/pdfengines/wa"
},
{
"path": "test/integration/features/prometheus_metrics.feature",
"chars": 6153,
"preview": "# TODO:\n# 1. Count restarts.\n# 2. Count queue size.\n\n@prometheus-metrics\nFeature: /prometheus/metrics\n\n Scenario: GET /"
},
{
"path": "test/integration/features/root.feature",
"chars": 2891,
"preview": "@root\nFeature: /\n\n Scenario: GET /\n Given I have a default Gotenberg container\n When I make a \"GET\" request to Go"
},
{
"path": "test/integration/features/version.feature",
"chars": 1551,
"preview": "@version\nFeature: /version\n\n Scenario: GET /version\n Given I have a default Gotenberg container\n When I make a \"G"
},
{
"path": "test/integration/features/webhook.feature",
"chars": 2722,
"preview": "# TODO:\n# 1. Other HTTP Methods\n# 2. Errors\n\n@webhook\nFeature: Webhook\n\n Scenario: Default\n Given I have a default G"
},
{
"path": "test/integration/main_test.go",
"chars": 1258,
"preview": "//go:build integration\n\npackage integration\n\nimport (\n\t\"os\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/cucumber/godog\"\n\t\"github"
},
{
"path": "test/integration/scenario/compare.go",
"chars": 1500,
"preview": "package scenario\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\nfunc compareJson(expected, actual any) error {\n\t// Handle maps (JSON obj"
},
{
"path": "test/integration/scenario/containers.go",
"chars": 3787,
"preview": "package scenario\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/docker/docker/api/types/contai"
},
{
"path": "test/integration/scenario/doc.go",
"chars": 77,
"preview": "// Package scenario gathers all steps used in the features.\npackage scenario\n"
},
{
"path": "test/integration/scenario/http.go",
"chars": 1911,
"preview": "package scenario\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\nfunc doRequest("
},
{
"path": "test/integration/scenario/scenario.go",
"chars": 32750,
"preview": "package scenario\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t"
},
{
"path": "test/integration/scenario/server.go",
"chars": 4905,
"preview": "package scenario\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings"
},
{
"path": "test/integration/testdata/embed_1.xml",
"chars": 90,
"preview": "<xml>\n <test>test 1.1</test>\n <test>test 1.2</test>\n <test>test 1.3</test>\n</xml>"
},
{
"path": "test/integration/testdata/embed_2.xml",
"chars": 90,
"preview": "<xml>\n <test>test 2.1</test>\n <test>test 2.2</test>\n <test>test 2.3</test>\n</xml>"
},
{
"path": "test/integration/testdata/feature-rich-html/index.html",
"chars": 2466,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <!-- net::ERR_FILE_NOT_FOUND -->\n <link re"
},
{
"path": "test/integration/testdata/feature-rich-html-remote/index.html",
"chars": 2181,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <!-- net::ERR_CONNECTION_REFUSED -->\n <lin"
},
{
"path": "test/integration/testdata/feature-rich-markdown/index.html",
"chars": 2014,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <!-- net::ERR_FILE_NOT_FOUND -->\n <link re"
},
{
"path": "test/integration/testdata/feature-rich-markdown/table.md",
"chars": 242,
"preview": "## This paragraph displays a table from a markdown file\n\n| Tables | Are | Cool |\n| -------- | :-----------:"
},
{
"path": "test/integration/testdata/header-footer-html/footer.html",
"chars": 231,
"preview": "<html>\n <head>\n <style>\n body {\n font-size: 12px;\n margin: auto 20px;\n }\n </style>\n </he"
},
{
"path": "test/integration/testdata/header-footer-html/header.html",
"chars": 214,
"preview": "<html>\n <head>\n <style>\n body {\n font-size: 12px;\n margin: auto 20px;\n }\n </style>\n </he"
},
{
"path": "test/integration/testdata/page-1-html/index.html",
"chars": 125,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Page 1</title>\n </head>\n <body>\n <h1>Page 1</h1>\n </body>\n</h"
},
{
"path": "test/integration/testdata/page-1-markdown/index.html",
"chars": 134,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Page 1</title>\n </head>\n <body>\n {{ toHTML \"page_1.md\" }}\n </"
},
{
"path": "test/integration/testdata/page-1-markdown/page_1.md",
"chars": 9,
"preview": "# Page 1\n"
},
{
"path": "test/integration/testdata/pages-12-html/index.html",
"chars": 743,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Pages 12</title>\n <style>\n .page-break-after {\n page"
},
{
"path": "test/integration/testdata/pages-12-markdown/index.html",
"chars": 983,
"preview": "<!doctype html>\n<html lang=\"en\">\n <head>\n <title>Pages 12</title>\n <style>\n .page-break-after {\n page"
},
{
"path": "test/integration/testdata/pages-12-markdown/page_1.md",
"chars": 9,
"preview": "# Page 1\n"
},
{
"path": "test/integration/testdata/pages-12-markdown/page_10.md",
"chars": 10,
"preview": "# Page 10\n"
},
{
"path": "test/integration/testdata/pages-12-markdown/page_11.md",
"chars": 10,
"preview": "# Page 11\n"
},
{
"path": "test/integration/testdata/pages-12-markdown/page_12.md",
"chars": 10,
"preview": "# Page 12\n"
},
{
"path": "test/integration/testdata/pages-12-markdown/page_2.md",
"chars": 9,
"preview": "# Page 2\n"
}
]
// ... and 74 more files (download for full content)
About this extraction
This page contains the full source code of the gotenberg/gotenberg GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 274 files (1.2 MB), approximately 327.7k tokens, and a symbol index with 757 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.