mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 20:52:14 +01:00
feat(chromium): capture screenshot (#765)
This commit is contained in:
10
go.mod
10
go.mod
@@ -13,21 +13,21 @@ require (
|
|||||||
github.com/hashicorp/go-retryablehttp v0.7.5
|
github.com/hashicorp/go-retryablehttp v0.7.5
|
||||||
github.com/klauspost/compress v1.17.4 // indirect
|
github.com/klauspost/compress v1.17.4 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/labstack/echo/v4 v4.11.3
|
github.com/labstack/echo/v4 v4.11.4
|
||||||
github.com/labstack/gommon v0.4.1
|
github.com/labstack/gommon v0.4.2
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mholt/archiver/v3 v3.5.1
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/nwaples/rardecode v1.1.3 // indirect
|
github.com/nwaples/rardecode v1.1.3 // indirect
|
||||||
github.com/pdfcpu/pdfcpu v0.6.0
|
github.com/pdfcpu/pdfcpu v0.6.0
|
||||||
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
||||||
github.com/prometheus/client_golang v1.17.0
|
github.com/prometheus/client_golang v1.18.0
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
github.com/ulikunitz/xz v0.5.11 // indirect
|
github.com/ulikunitz/xz v0.5.11 // indirect
|
||||||
go.uber.org/multierr v1.11.0
|
go.uber.org/multierr v1.11.0
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.16.0 // indirect
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
golang.org/x/image v0.14.0 // indirect
|
golang.org/x/image v0.14.0 // indirect
|
||||||
golang.org/x/net v0.19.0
|
golang.org/x/net v0.19.0
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.5.0
|
||||||
@@ -61,6 +61,6 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.32.0 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
22
go.sum
22
go.sum
@@ -28,7 +28,6 @@ github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
|
|||||||
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
github.com/gobwas/ws v1.3.1 h1:Qi34dfLMWJbiKaNbDVzM9x27nZBjmkaW6i4+Ku+pGVU=
|
||||||
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
github.com/gobwas/ws v1.3.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
|
||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||||
@@ -63,10 +62,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM=
|
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
|
||||||
github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws=
|
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
|
||||||
github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM=
|
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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
@@ -98,8 +97,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||||
@@ -136,8 +135,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
|||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||||
golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
|
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||||
golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||||
@@ -153,9 +152,8 @@ golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
|||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import (
|
|||||||
|
|
||||||
type browser interface {
|
type browser interface {
|
||||||
gotenberg.Process
|
gotenberg.Process
|
||||||
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type browserArguments struct {
|
type browserArguments struct {
|
||||||
@@ -213,9 +214,53 @@ func (b *chromiumBrowser) Healthy(logger *zap.Logger) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
|
||||||
|
network.Enable(),
|
||||||
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
||||||
|
forceExactColorsActionFunc(),
|
||||||
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
// PDF specific.
|
||||||
|
printToPdfActionFunc(logger, outputPath, options),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return b.do(ctx, logger, url, options.Options, chromedp.Tasks{
|
||||||
|
network.Enable(),
|
||||||
|
fetch.Enable(),
|
||||||
|
runtime.Enable(),
|
||||||
|
clearCacheActionFunc(logger, b.arguments.clearCache),
|
||||||
|
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
||||||
|
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
||||||
|
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
||||||
|
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
||||||
|
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
|
||||||
|
forceExactColorsActionFunc(),
|
||||||
|
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
||||||
|
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
||||||
|
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
||||||
|
// Screenshot specific.
|
||||||
|
captureScreenshotActionFunc(logger, outputPath, options),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *chromiumBrowser) do(ctx context.Context, logger *zap.Logger, url string, options Options, tasks chromedp.Tasks) error {
|
||||||
if !b.isStarted.Load() {
|
if !b.isStarted.Load() {
|
||||||
return errors.New("browser not started, cannot handle PDF conversion")
|
return errors.New("browser not started, cannot handle tasks")
|
||||||
}
|
}
|
||||||
|
|
||||||
// We validate the "main" URL against our allow / deny lists.
|
// We validate the "main" URL against our allow / deny lists.
|
||||||
@@ -265,23 +310,6 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
listenForEventExceptionThrown(taskCtx, logger, &consoleExceptions, &consoleExceptionsMu)
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks := chromedp.Tasks{
|
|
||||||
network.Enable(),
|
|
||||||
fetch.Enable(),
|
|
||||||
runtime.Enable(),
|
|
||||||
clearCacheActionFunc(logger, b.arguments.clearCache),
|
|
||||||
clearCookiesActionFunc(logger, b.arguments.clearCookies),
|
|
||||||
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
|
|
||||||
extraHttpHeadersActionFunc(logger, options.ExtraHttpHeaders),
|
|
||||||
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent),
|
|
||||||
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
|
|
||||||
forceExactColorsActionFunc(),
|
|
||||||
emulateMediaTypeActionFunc(logger, options.EmulatedMediaType),
|
|
||||||
waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay),
|
|
||||||
waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression),
|
|
||||||
printToPdfActionFunc(logger, outputPath, options),
|
|
||||||
}
|
|
||||||
|
|
||||||
err := chromedp.Run(taskCtx, tasks...)
|
err := chromedp.Run(taskCtx, tasks...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errMessage := err.Error()
|
errMessage := err.Error()
|
||||||
@@ -298,7 +326,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp
|
|||||||
return ErrRpccMessageTooLarge
|
return ErrRpccMessageTooLarge
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("print to PDF: %w", err)
|
return fmt.Errorf("handle tasks: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/613.
|
// See https://github.com/gotenberg/gotenberg/issues/613.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -24,10 +24,6 @@ var (
|
|||||||
// allowed/denied lists.
|
// allowed/denied lists.
|
||||||
ErrUrlNotAuthorized = errors.New("URL not authorized")
|
ErrUrlNotAuthorized = errors.New("URL not authorized")
|
||||||
|
|
||||||
// ErrOmitBackgroundWithoutPrintBackground happens if
|
|
||||||
// Options.OmitBackground is set to true but not Options.PrintBackground.
|
|
||||||
ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background")
|
|
||||||
|
|
||||||
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
// ErrInvalidEmulatedMediaType happens if the emulated media type is not
|
||||||
// "screen" nor "print". Empty value are allowed though.
|
// "screen" nor "print". Empty value are allowed though.
|
||||||
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
ErrInvalidEmulatedMediaType = errors.New("invalid emulated media type")
|
||||||
@@ -36,14 +32,6 @@ var (
|
|||||||
// returns an exception or undefined.
|
// returns an exception or undefined.
|
||||||
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
|
ErrInvalidEvaluationExpression = errors.New("invalid evaluation expression")
|
||||||
|
|
||||||
// ErrInvalidPrinterSettings happens if the Options have one or more
|
|
||||||
// aberrant values.
|
|
||||||
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
|
||||||
|
|
||||||
// ErrPageRangesSyntaxError happens if the Options have an invalid page
|
|
||||||
// ranges.
|
|
||||||
ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
|
|
||||||
|
|
||||||
// ErrRpccMessageTooLarge happens when the messages received by
|
// ErrRpccMessageTooLarge happens when the messages received by
|
||||||
// ChromeDevTools are larger than 100 MB.
|
// ChromeDevTools are larger than 100 MB.
|
||||||
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
ErrRpccMessageTooLarge = errors.New("rpcc message too large")
|
||||||
@@ -56,6 +44,20 @@ var (
|
|||||||
// console. It also happens only if the [Options.FailOnConsoleExceptions]
|
// console. It also happens only if the [Options.FailOnConsoleExceptions]
|
||||||
// is set to true.
|
// is set to true.
|
||||||
ErrConsoleExceptions = errors.New("console exceptions")
|
ErrConsoleExceptions = errors.New("console exceptions")
|
||||||
|
|
||||||
|
// PDF specific.
|
||||||
|
|
||||||
|
// ErrOmitBackgroundWithoutPrintBackground happens if
|
||||||
|
// PdfOptions.OmitBackground is set to true but not PdfOptions.PrintBackground.
|
||||||
|
ErrOmitBackgroundWithoutPrintBackground = errors.New("omit background without print background")
|
||||||
|
|
||||||
|
// ErrInvalidPrinterSettings happens if the PdfOptions have one or more
|
||||||
|
// aberrant values.
|
||||||
|
ErrInvalidPrinterSettings = errors.New("invalid printer settings")
|
||||||
|
|
||||||
|
// ErrPageRangesSyntaxError happens if the PdfOptions have an invalid page
|
||||||
|
// ranges.
|
||||||
|
ErrPageRangesSyntaxError = errors.New("page ranges syntax error")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chromium is a module which provides both an [Api] and routes for converting
|
// Chromium is a module which provides both an [Api] and routes for converting
|
||||||
@@ -71,7 +73,7 @@ type Chromium struct {
|
|||||||
engine gotenberg.PdfEngine
|
engine gotenberg.PdfEngine
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options are the available expectedOptions for converting HTML document to PDF.
|
// Options are the common options for all conversions.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// SkipNetworkIdleEvent set if the conversion should wait for the
|
// SkipNetworkIdleEvent set if the conversion should wait for the
|
||||||
// "networkIdle" event, drastically improving the conversion speed. It may
|
// "networkIdle" event, drastically improving the conversion speed. It may
|
||||||
@@ -91,17 +93,17 @@ type Options struct {
|
|||||||
FailOnConsoleExceptions bool
|
FailOnConsoleExceptions bool
|
||||||
|
|
||||||
// WaitDelay is the duration to wait when loading an HTML document before
|
// WaitDelay is the duration to wait when loading an HTML document before
|
||||||
// converting it to PDF.
|
// converting it.
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitDelay time.Duration
|
WaitDelay time.Duration
|
||||||
|
|
||||||
// WaitWindowStatus is the window.status value to wait for before
|
// WaitWindowStatus is the window.status value to wait for before
|
||||||
// converting an HTML document to PDF.
|
// converting an HTML document.
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitWindowStatus string
|
WaitWindowStatus string
|
||||||
|
|
||||||
// WaitForExpression is the custom JavaScript expression to wait before
|
// WaitForExpression is the custom JavaScript expression to wait before
|
||||||
// converting an HTML document to PDF until it returns true
|
// converting an HTML document until it returns true
|
||||||
// Optional.
|
// Optional.
|
||||||
WaitForExpression string
|
WaitForExpression string
|
||||||
|
|
||||||
@@ -115,6 +117,31 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
EmulatedMediaType string
|
EmulatedMediaType string
|
||||||
|
|
||||||
|
// OmitBackground hides default white background and allows generating PDFs
|
||||||
|
// with transparency.
|
||||||
|
// Optional.
|
||||||
|
OmitBackground bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions returns the default values for Options.
|
||||||
|
func DefaultOptions() Options {
|
||||||
|
return Options{
|
||||||
|
SkipNetworkIdleEvent: false,
|
||||||
|
FailOnHttpStatusCodes: []int64{499, 599},
|
||||||
|
FailOnConsoleExceptions: false,
|
||||||
|
WaitDelay: 0,
|
||||||
|
WaitWindowStatus: "",
|
||||||
|
WaitForExpression: "",
|
||||||
|
ExtraHttpHeaders: nil,
|
||||||
|
EmulatedMediaType: "",
|
||||||
|
OmitBackground: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PdfOptions are the available options for converting an HTML document to PDF.
|
||||||
|
type PdfOptions struct {
|
||||||
|
Options
|
||||||
|
|
||||||
// Landscape sets the paper orientation.
|
// Landscape sets the paper orientation.
|
||||||
// Optional.
|
// Optional.
|
||||||
Landscape bool
|
Landscape bool
|
||||||
@@ -123,11 +150,6 @@ type Options struct {
|
|||||||
// Optional.
|
// Optional.
|
||||||
PrintBackground bool
|
PrintBackground bool
|
||||||
|
|
||||||
// OmitBackground hides default white background and allows generating PDFs
|
|
||||||
// with transparency.
|
|
||||||
// Optional.
|
|
||||||
OmitBackground bool
|
|
||||||
|
|
||||||
// Scale is the scale of the page rendering.
|
// Scale is the scale of the page rendering.
|
||||||
// Optional.
|
// Optional.
|
||||||
Scale float64
|
Scale float64
|
||||||
@@ -184,37 +206,60 @@ type Options struct {
|
|||||||
PreferCssPageSize bool
|
PreferCssPageSize bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOptions returns the default values for Options.
|
// DefaultPdfOptions returns the default values for PdfOptions.
|
||||||
func DefaultOptions() Options {
|
func DefaultPdfOptions() PdfOptions {
|
||||||
return Options{
|
return PdfOptions{
|
||||||
SkipNetworkIdleEvent: false,
|
Options: DefaultOptions(),
|
||||||
FailOnHttpStatusCodes: []int64{499, 599},
|
Landscape: false,
|
||||||
FailOnConsoleExceptions: false,
|
PrintBackground: false,
|
||||||
WaitDelay: 0,
|
Scale: 1.0,
|
||||||
WaitWindowStatus: "",
|
PaperWidth: 8.5,
|
||||||
WaitForExpression: "",
|
PaperHeight: 11,
|
||||||
ExtraHttpHeaders: nil,
|
MarginTop: 0.39,
|
||||||
EmulatedMediaType: "",
|
MarginBottom: 0.39,
|
||||||
Landscape: false,
|
MarginLeft: 0.39,
|
||||||
PrintBackground: false,
|
MarginRight: 0.39,
|
||||||
OmitBackground: false,
|
PageRanges: "",
|
||||||
Scale: 1.0,
|
HeaderTemplate: "<html><head></head><body></body></html>",
|
||||||
PaperWidth: 8.5,
|
FooterTemplate: "<html><head></head><body></body></html>",
|
||||||
PaperHeight: 11,
|
PreferCssPageSize: false,
|
||||||
MarginTop: 0.39,
|
}
|
||||||
MarginBottom: 0.39,
|
}
|
||||||
MarginLeft: 0.39,
|
|
||||||
MarginRight: 0.39,
|
// ScreenshotOptions are the available options for capturing a screenshot from
|
||||||
PageRanges: "",
|
// an HTML document.
|
||||||
HeaderTemplate: "<html><head></head><body></body></html>",
|
type ScreenshotOptions struct {
|
||||||
FooterTemplate: "<html><head></head><body></body></html>",
|
Options
|
||||||
PreferCssPageSize: false,
|
|
||||||
|
// Format is the image compression format, either "png" or "jpeg" or
|
||||||
|
// "webp".
|
||||||
|
// Optional.
|
||||||
|
Format string
|
||||||
|
|
||||||
|
// Quality is the compression quality from range [0..100] (jpeg only).
|
||||||
|
// Optional.
|
||||||
|
Quality int
|
||||||
|
|
||||||
|
// OptimizeForSpeed defines whether to optimize image encoding for speed,
|
||||||
|
// not for resulting size.
|
||||||
|
// Optional.
|
||||||
|
OptimizeForSpeed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultScreenshotOptions returns the default values for ScreenshotOptions.
|
||||||
|
func DefaultScreenshotOptions() ScreenshotOptions {
|
||||||
|
return ScreenshotOptions{
|
||||||
|
Options: DefaultOptions(),
|
||||||
|
Format: "png",
|
||||||
|
Quality: 100,
|
||||||
|
OptimizeForSpeed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
// Api helps to interact with Chromium for converting HTML documents to PDF.
|
||||||
type Api interface {
|
type Api interface {
|
||||||
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provider is a module interface which exposes a method for creating an [Api]
|
// Provider is a module interface which exposes a method for creating an [Api]
|
||||||
@@ -443,19 +488,31 @@ func (mod *Chromium) Routes() ([]api.Route, error) {
|
|||||||
|
|
||||||
return []api.Route{
|
return []api.Route{
|
||||||
convertUrlRoute(mod, mod.engine),
|
convertUrlRoute(mod, mod.engine),
|
||||||
|
screenshotUrlRoute(mod),
|
||||||
convertHtmlRoute(mod, mod.engine),
|
convertHtmlRoute(mod, mod.engine),
|
||||||
|
screenshotHtmlRoute(mod),
|
||||||
convertMarkdownRoute(mod, mod.engine),
|
convertMarkdownRoute(mod, mod.engine),
|
||||||
|
screenshotMarkdownRoute(mod),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pdf converts a URL to PDF.
|
// Pdf converts a URL to PDF.
|
||||||
func (mod *Chromium) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (mod *Chromium) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
// Note: no error wrapping because it leaks on console exceptions output.
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
return mod.supervisor.Run(ctx, logger, func() error {
|
return mod.supervisor.Run(ctx, logger, func() error {
|
||||||
return mod.browser.pdf(ctx, logger, url, outputPath, options)
|
return mod.browser.pdf(ctx, logger, url, outputPath, options)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mod *Chromium) Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
// Note: no error wrapping because it leaks on errors we want to display to
|
||||||
|
// the end user.
|
||||||
|
return mod.supervisor.Run(ctx, logger, func() error {
|
||||||
|
return mod.browser.screenshot(ctx, logger, url, outputPath, options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ gotenberg.Module = (*Chromium)(nil)
|
_ gotenberg.Module = (*Chromium)(nil)
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDefaultOptions(t *testing.T) {
|
func TestDefaultOptions(t *testing.T) {
|
||||||
actual := DefaultOptions()
|
actual := DefaultPdfOptions()
|
||||||
notExpect := Options{}
|
notExpect := PdfOptions{}
|
||||||
|
|
||||||
if reflect.DeepEqual(actual, notExpect) {
|
if reflect.DeepEqual(actual, notExpect) {
|
||||||
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
t.Errorf("expected %v and got identical %v", actual, notExpect)
|
||||||
@@ -461,7 +461,7 @@ func TestChromium_Routes(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "routes not disabled",
|
scenario: "routes not disabled",
|
||||||
expectRoutes: 3,
|
expectRoutes: 6,
|
||||||
disableRoutes: false,
|
disableRoutes: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -495,14 +495,14 @@ func TestChromium_Pdf(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
scenario: "PDF task success",
|
scenario: "PDF task success",
|
||||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
expectError: false,
|
expectError: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
scenario: "PDF task error",
|
scenario: "PDF task error",
|
||||||
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
browser: &browserMock{pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return errors.New("PDF task error")
|
return errors.New("PDF task error")
|
||||||
}},
|
}},
|
||||||
expectError: true,
|
expectError: true,
|
||||||
@@ -515,7 +515,49 @@ func TestChromium_Pdf(t *testing.T) {
|
|||||||
}}
|
}}
|
||||||
mod.browser = tc.browser
|
mod.browser = tc.browser
|
||||||
|
|
||||||
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
err := mod.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
|
|
||||||
|
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 TestChromium_Screenshot(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
scenario string
|
||||||
|
supervisor gotenberg.ProcessSupervisor
|
||||||
|
browser browser
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
scenario: "Screenshot task success",
|
||||||
|
browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scenario: "Screenshot task error",
|
||||||
|
browser: &browserMock{screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return errors.New("screenshot task error")
|
||||||
|
}},
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tc.scenario, func(t *testing.T) {
|
||||||
|
mod := new(Chromium)
|
||||||
|
mod.supervisor = &gotenberg.ProcessSupervisorMock{RunMock: func(ctx context.Context, logger *zap.Logger, task func() error) error {
|
||||||
|
return task()
|
||||||
|
}}
|
||||||
|
mod.browser = tc.browser
|
||||||
|
|
||||||
|
err := mod.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||||
|
|
||||||
if !tc.expectError && err != nil {
|
if !tc.expectError && err != nil {
|
||||||
t.Fatalf("expected no error but got: %v", err)
|
t.Fatalf("expected no error but got: %v", err)
|
||||||
|
|||||||
@@ -10,23 +10,33 @@ import (
|
|||||||
|
|
||||||
// ApiMock is a mock for the [Api] interface.
|
// ApiMock is a mock for the [Api] interface.
|
||||||
type ApiMock struct {
|
type ApiMock struct {
|
||||||
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
PdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
ScreenshotMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (api *ApiMock) Pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return api.PdfMock(ctx, logger, url, outputPath, options)
|
return api.PdfMock(ctx, logger, url, outputPath, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *ApiMock) Screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return api.ScreenshotMock(ctx, logger, url, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
// browserMock is a mock for the [browser] interface.
|
// browserMock is a mock for the [browser] interface.
|
||||||
type browserMock struct {
|
type browserMock struct {
|
||||||
gotenberg.ProcessMock
|
gotenberg.ProcessMock
|
||||||
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error
|
pdfMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error
|
||||||
|
screenshotMock func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
func (b *browserMock) pdf(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
return b.pdfMock(ctx, logger, url, outputPath, options)
|
return b.pdfMock(ctx, logger, url, outputPath, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *browserMock) screenshot(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
|
return b.screenshotMock(ctx, logger, url, outputPath, options)
|
||||||
|
}
|
||||||
|
|
||||||
// Interface guards.
|
// Interface guards.
|
||||||
var (
|
var (
|
||||||
_ Api = (*ApiMock)(nil)
|
_ Api = (*ApiMock)(nil)
|
||||||
|
|||||||
@@ -9,26 +9,42 @@ import (
|
|||||||
|
|
||||||
func TestApiMock(t *testing.T) {
|
func TestApiMock(t *testing.T) {
|
||||||
mock := &ApiMock{
|
mock := &ApiMock{
|
||||||
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
PdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ScreenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
err := mock.Pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
t.Errorf("expected no error from ApiMock.Pdf, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mock.Screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from ApiMock.Screenshot, but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBrowserMock(t *testing.T) {
|
func TestBrowserMock(t *testing.T) {
|
||||||
mock := &browserMock{
|
mock := &browserMock{
|
||||||
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options Options) error {
|
pdfMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options PdfOptions) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
screenshotMock: func(ctx context.Context, logger *zap.Logger, url, outputPath string, options ScreenshotOptions) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mock.pdf(context.Background(), zap.NewNop(), "", "", Options{})
|
err := mock.pdf(context.Background(), zap.NewNop(), "", "", PdfOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
|
t.Errorf("expected no error from browserMock.pdf, but got: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = mock.screenshot(context.Background(), zap.NewNop(), "", "", ScreenshotOptions{})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error from browserMock.screenshot, but got: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -21,26 +22,21 @@ import (
|
|||||||
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
"github.com/gotenberg/gotenberg/v7/pkg/modules/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FormDataChromiumPdfOptions creates [Options] from the form data. Fallback to
|
// FormDataChromiumOptions creates [Options] from the form data. Fallback to
|
||||||
// default value if the considered key is not present.
|
// default value if the considered key is not present.
|
||||||
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) {
|
||||||
defaultOptions := DefaultOptions()
|
defaultOptions := DefaultOptions()
|
||||||
|
|
||||||
var (
|
var (
|
||||||
skipNetworkIdleEvent bool
|
skipNetworkIdleEvent bool
|
||||||
failOnHttpStatusCodes []int64
|
failOnHttpStatusCodes []int64
|
||||||
failOnConsoleExceptions bool
|
failOnConsoleExceptions bool
|
||||||
waitDelay time.Duration
|
waitDelay time.Duration
|
||||||
waitWindowStatus string
|
waitWindowStatus string
|
||||||
waitForExpression string
|
waitForExpression string
|
||||||
extraHttpHeaders map[string]string
|
extraHttpHeaders map[string]string
|
||||||
emulatedMediaType string
|
emulatedMediaType string
|
||||||
landscape, printBackground, omitBackground bool
|
omitBackground bool
|
||||||
scale, paperWidth, paperHeight float64
|
|
||||||
marginTop, marginBottom, marginLeft, marginRight float64
|
|
||||||
pageRanges string
|
|
||||||
headerTemplate, footerTemplate string
|
|
||||||
preferCssPageSize bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
form := ctx.FormData().
|
form := ctx.FormData().
|
||||||
@@ -89,20 +85,7 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}).
|
}).
|
||||||
Bool("landscape", &landscape, defaultOptions.Landscape).
|
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground)
|
||||||
Bool("printBackground", &printBackground, defaultOptions.PrintBackground).
|
|
||||||
Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground).
|
|
||||||
Float64("scale", &scale, defaultOptions.Scale).
|
|
||||||
Float64("paperWidth", &paperWidth, defaultOptions.PaperWidth).
|
|
||||||
Float64("paperHeight", &paperHeight, defaultOptions.PaperHeight).
|
|
||||||
Float64("marginTop", &marginTop, defaultOptions.MarginTop).
|
|
||||||
Float64("marginBottom", &marginBottom, defaultOptions.MarginBottom).
|
|
||||||
Float64("marginLeft", &marginLeft, defaultOptions.MarginLeft).
|
|
||||||
Float64("marginRight", &marginRight, defaultOptions.MarginRight).
|
|
||||||
String("nativePageRanges", &pageRanges, defaultOptions.PageRanges).
|
|
||||||
Content("header.html", &headerTemplate, defaultOptions.HeaderTemplate).
|
|
||||||
Content("footer.html", &footerTemplate, defaultOptions.FooterTemplate).
|
|
||||||
Bool("preferCssPageSize", &preferCssPageSize, defaultOptions.PreferCssPageSize)
|
|
||||||
|
|
||||||
options := Options{
|
options := Options{
|
||||||
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
SkipNetworkIdleEvent: skipNetworkIdleEvent,
|
||||||
@@ -113,34 +96,132 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, Options) {
|
|||||||
WaitForExpression: waitForExpression,
|
WaitForExpression: waitForExpression,
|
||||||
ExtraHttpHeaders: extraHttpHeaders,
|
ExtraHttpHeaders: extraHttpHeaders,
|
||||||
EmulatedMediaType: emulatedMediaType,
|
EmulatedMediaType: emulatedMediaType,
|
||||||
Landscape: landscape,
|
|
||||||
PrintBackground: printBackground,
|
|
||||||
OmitBackground: omitBackground,
|
OmitBackground: omitBackground,
|
||||||
Scale: scale,
|
|
||||||
PaperWidth: paperWidth,
|
|
||||||
PaperHeight: paperHeight,
|
|
||||||
MarginTop: marginTop,
|
|
||||||
MarginBottom: marginBottom,
|
|
||||||
MarginLeft: marginLeft,
|
|
||||||
MarginRight: marginRight,
|
|
||||||
PageRanges: pageRanges,
|
|
||||||
HeaderTemplate: headerTemplate,
|
|
||||||
FooterTemplate: footerTemplate,
|
|
||||||
PreferCssPageSize: preferCssPageSize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return form, options
|
return form, options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FormDataChromiumPdfOptions creates [PdfOptions] from the form data. Fallback to
|
||||||
|
// default value if the considered key is not present.
|
||||||
|
func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) {
|
||||||
|
form, options := FormDataChromiumOptions(ctx)
|
||||||
|
defaultPdfOptions := DefaultPdfOptions()
|
||||||
|
|
||||||
|
var (
|
||||||
|
landscape, printBackground bool
|
||||||
|
scale, paperWidth, paperHeight float64
|
||||||
|
marginTop, marginBottom, marginLeft, marginRight float64
|
||||||
|
pageRanges string
|
||||||
|
headerTemplate, footerTemplate string
|
||||||
|
preferCssPageSize bool
|
||||||
|
)
|
||||||
|
|
||||||
|
form.
|
||||||
|
Bool("landscape", &landscape, defaultPdfOptions.Landscape).
|
||||||
|
Bool("printBackground", &printBackground, defaultPdfOptions.PrintBackground).
|
||||||
|
Float64("scale", &scale, defaultPdfOptions.Scale).
|
||||||
|
Float64("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth).
|
||||||
|
Float64("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight).
|
||||||
|
Float64("marginTop", &marginTop, defaultPdfOptions.MarginTop).
|
||||||
|
Float64("marginBottom", &marginBottom, defaultPdfOptions.MarginBottom).
|
||||||
|
Float64("marginLeft", &marginLeft, defaultPdfOptions.MarginLeft).
|
||||||
|
Float64("marginRight", &marginRight, defaultPdfOptions.MarginRight).
|
||||||
|
String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges).
|
||||||
|
Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate).
|
||||||
|
Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).
|
||||||
|
Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize)
|
||||||
|
|
||||||
|
pdfOptions := PdfOptions{
|
||||||
|
Options: options,
|
||||||
|
Landscape: landscape,
|
||||||
|
PrintBackground: printBackground,
|
||||||
|
Scale: scale,
|
||||||
|
PaperWidth: paperWidth,
|
||||||
|
PaperHeight: paperHeight,
|
||||||
|
MarginTop: marginTop,
|
||||||
|
MarginBottom: marginBottom,
|
||||||
|
MarginLeft: marginLeft,
|
||||||
|
MarginRight: marginRight,
|
||||||
|
PageRanges: pageRanges,
|
||||||
|
HeaderTemplate: headerTemplate,
|
||||||
|
FooterTemplate: footerTemplate,
|
||||||
|
PreferCssPageSize: preferCssPageSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
return form, pdfOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormDataChromiumScreenshotOptions creates [ScreenshotOptions] from the form
|
||||||
|
// data. Fallback to default value if the considered key is not present.
|
||||||
|
func FormDataChromiumScreenshotOptions(ctx *api.Context) (*api.FormData, ScreenshotOptions) {
|
||||||
|
form, options := FormDataChromiumOptions(ctx)
|
||||||
|
defaultScreenshotOptions := DefaultScreenshotOptions()
|
||||||
|
|
||||||
|
var (
|
||||||
|
format string
|
||||||
|
quality int
|
||||||
|
optimizeForSpeed bool
|
||||||
|
)
|
||||||
|
|
||||||
|
form.
|
||||||
|
Custom("format", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
format = defaultScreenshotOptions.Format
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if value != "png" && value != "jpeg" && value != "" {
|
||||||
|
return fmt.Errorf("wrong value, expected either 'png', 'jpeg' or 'webp'")
|
||||||
|
}
|
||||||
|
|
||||||
|
format = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Custom("quality", func(value string) error {
|
||||||
|
if value == "" {
|
||||||
|
quality = defaultScreenshotOptions.Quality
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
intValue, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if intValue < 0 {
|
||||||
|
return errors.New("value is negative")
|
||||||
|
}
|
||||||
|
|
||||||
|
if intValue > 100 {
|
||||||
|
return errors.New("value is superior to 100")
|
||||||
|
}
|
||||||
|
|
||||||
|
quality = intValue
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
Bool("optimizeForSpeed", &optimizeForSpeed, defaultScreenshotOptions.OptimizeForSpeed)
|
||||||
|
|
||||||
|
screenshotOptions := ScreenshotOptions{
|
||||||
|
Options: options,
|
||||||
|
Format: format,
|
||||||
|
Quality: quality,
|
||||||
|
OptimizeForSpeed: optimizeForSpeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
return form, screenshotOptions
|
||||||
|
}
|
||||||
|
|
||||||
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
// FormDataChromiumPdfFormats creates [gotenberg.PdfFormats] from the form
|
||||||
// data. Fallback to default value if the considered key is not present.
|
// data. Fallback to default value if the considered key is not present.
|
||||||
func FormDataChromiumPdfFormats(ctx *api.Context) gotenberg.PdfFormats {
|
func FormDataChromiumPdfFormats(form *api.FormData) gotenberg.PdfFormats {
|
||||||
var (
|
var (
|
||||||
pdfa string
|
pdfa string
|
||||||
pdfua bool
|
pdfua bool
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.FormData().
|
form.
|
||||||
String("pdfa", &pdfa, "").
|
String("pdfa", &pdfa, "").
|
||||||
Bool("pdfua", &pdfua, false)
|
Bool("pdfua", &pdfua, false)
|
||||||
|
|
||||||
@@ -159,7 +240,7 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
|
||||||
var url string
|
var url string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -179,6 +260,35 @@ func convertUrlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// screenshotUrlRoute returns an [api.Route] which can take a screenshot from a
|
||||||
|
// URL.
|
||||||
|
func screenshotUrlRoute(chromium Api) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/chromium/screenshot/url",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
form, options := FormDataChromiumScreenshotOptions(ctx)
|
||||||
|
|
||||||
|
var url string
|
||||||
|
err := form.
|
||||||
|
MandatoryString("url", &url).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = screenshotUrl(ctx, chromium, url, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("URL screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
|
// convertHtmlRoute returns an [api.Route] which can convert an HTML file to
|
||||||
// PDF.
|
// PDF.
|
||||||
func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||||
@@ -189,7 +299,7 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
|
||||||
var inputPath string
|
var inputPath string
|
||||||
err := form.
|
err := form.
|
||||||
@@ -210,6 +320,36 @@ func convertHtmlRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// screenshotHtmlRoute returns an [api.Route] which can take a screenshot from
|
||||||
|
// an HTML file.
|
||||||
|
func screenshotHtmlRoute(chromium Api) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/chromium/screenshot/html",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
form, options := FormDataChromiumScreenshotOptions(ctx)
|
||||||
|
|
||||||
|
var inputPath string
|
||||||
|
err := form.
|
||||||
|
MandatoryPath("index.html", &inputPath).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("file://%s", inputPath)
|
||||||
|
err = screenshotUrl(ctx, chromium, url, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("HTML screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
|
// convertMarkdownRoute returns an [api.Route] which can convert markdown files
|
||||||
// to PDF.
|
// to PDF.
|
||||||
func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
||||||
@@ -220,7 +360,7 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
Handler: func(c echo.Context) error {
|
Handler: func(c echo.Context) error {
|
||||||
ctx := c.Get("context").(*api.Context)
|
ctx := c.Get("context").(*api.Context)
|
||||||
form, options := FormDataChromiumPdfOptions(ctx)
|
form, options := FormDataChromiumPdfOptions(ctx)
|
||||||
pdfFormats := FormDataChromiumPdfFormats(ctx)
|
pdfFormats := FormDataChromiumPdfFormats(form)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
inputPath string
|
inputPath string
|
||||||
@@ -235,79 +375,11 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
return fmt.Errorf("validate form data: %w", err)
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have to convert each markdown file referenced in the HTML
|
url, err := markdownToHtml(ctx, inputPath, markdownPaths)
|
||||||
// file to... HTML. Thanks to the "html/template" package, we are
|
|
||||||
// able to provide the "toHTML" function which the user may call
|
|
||||||
// directly inside the HTML file.
|
|
||||||
|
|
||||||
var markdownFilesNotFoundErr error
|
|
||||||
|
|
||||||
tmpl, err := template.
|
|
||||||
New(filepath.Base(inputPath)).
|
|
||||||
Funcs(template.FuncMap{
|
|
||||||
"toHTML": func(filename string) (template.HTML, error) {
|
|
||||||
var path string
|
|
||||||
|
|
||||||
for _, markdownPath := range markdownPaths {
|
|
||||||
markdownFilename := filepath.Base(markdownPath)
|
|
||||||
|
|
||||||
if filename == markdownFilename {
|
|
||||||
path = markdownPath
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if path == "" {
|
|
||||||
markdownFilesNotFoundErr = multierr.Append(
|
|
||||||
markdownFilesNotFoundErr,
|
|
||||||
fmt.Errorf("'%s'", filename),
|
|
||||||
)
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
b, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("read markdown file '%s': %w", filename, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe := blackfriday.Run(b)
|
|
||||||
sanitized := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
|
||||||
|
|
||||||
// #nosec
|
|
||||||
return template.HTML(sanitized), nil
|
|
||||||
},
|
|
||||||
}).ParseFiles(inputPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("parse template file: %w", err)
|
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
|
|
||||||
err = tmpl.Execute(&buffer, &struct{}{})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("execute template: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if markdownFilesNotFoundErr != nil {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Markdown file(s) not found: %s", markdownFilesNotFoundErr),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
inputPath = ctx.GeneratePath(".html")
|
|
||||||
|
|
||||||
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("write template result: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := fmt.Sprintf("file://%s", inputPath)
|
|
||||||
|
|
||||||
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
err = convertUrl(ctx, chromium, engine, url, pdfFormats, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert markdown to PDF: %w", err)
|
return fmt.Errorf("convert markdown to PDF: %w", err)
|
||||||
@@ -318,26 +390,129 @@ func convertMarkdownRoute(chromium Api, engine gotenberg.PdfEngine) api.Route {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertUrl is a stub which is called by the other methods of this file.
|
// screenshotMarkdownRoute returns an [api.Route] which can take a screenshot
|
||||||
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options Options) error {
|
// from markdown files.
|
||||||
|
func screenshotMarkdownRoute(chromium Api) api.Route {
|
||||||
|
return api.Route{
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Path: "/forms/chromium/screenshot/markdown",
|
||||||
|
IsMultipart: true,
|
||||||
|
Handler: func(c echo.Context) error {
|
||||||
|
ctx := c.Get("context").(*api.Context)
|
||||||
|
form, options := FormDataChromiumScreenshotOptions(ctx)
|
||||||
|
|
||||||
|
var (
|
||||||
|
inputPath string
|
||||||
|
markdownPaths []string
|
||||||
|
)
|
||||||
|
|
||||||
|
err := form.
|
||||||
|
MandatoryPath("index.html", &inputPath).
|
||||||
|
MandatoryPaths([]string{".md"}, &markdownPaths).
|
||||||
|
Validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validate form data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
url, err := markdownToHtml(ctx, inputPath, markdownPaths)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("transform markdown file(s) to HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = screenshotUrl(ctx, chromium, url, options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("markdown screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func markdownToHtml(ctx *api.Context, inputPath string, markdownPaths []string) (string, error) {
|
||||||
|
// We have to convert each markdown file referenced in the HTML
|
||||||
|
// file to... HTML. Thanks to the "html/template" package, we are
|
||||||
|
// able to provide the "toHTML" function which the user may call
|
||||||
|
// directly inside the HTML file.
|
||||||
|
|
||||||
|
var markdownFilesNotFoundErr error
|
||||||
|
|
||||||
|
tmpl, err := template.
|
||||||
|
New(filepath.Base(inputPath)).
|
||||||
|
Funcs(template.FuncMap{
|
||||||
|
"toHTML": func(filename string) (template.HTML, error) {
|
||||||
|
var path string
|
||||||
|
|
||||||
|
for _, markdownPath := range markdownPaths {
|
||||||
|
markdownFilename := filepath.Base(markdownPath)
|
||||||
|
|
||||||
|
if filename == markdownFilename {
|
||||||
|
path = markdownPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
markdownFilesNotFoundErr = multierr.Append(
|
||||||
|
markdownFilesNotFoundErr,
|
||||||
|
fmt.Errorf("'%s'", filename),
|
||||||
|
)
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("read markdown file '%s': %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe := blackfriday.Run(b)
|
||||||
|
sanitized := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
|
||||||
|
|
||||||
|
// #nosec
|
||||||
|
return template.HTML(sanitized), nil
|
||||||
|
},
|
||||||
|
}).ParseFiles(inputPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse template file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
|
err = tmpl.Execute(&buffer, &struct{}{})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("execute template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if markdownFilesNotFoundErr != nil {
|
||||||
|
return "", api.WrapError(
|
||||||
|
fmt.Errorf("markdown files not found: %w", markdownFilesNotFoundErr),
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Markdown file(s) not found: %s", markdownFilesNotFoundErr),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
inputPath = ctx.GeneratePath(".html")
|
||||||
|
|
||||||
|
err = os.WriteFile(inputPath, buffer.Bytes(), 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("write template result: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("file://%s", inputPath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url string, pdfFormats gotenberg.PdfFormats, options PdfOptions) error {
|
||||||
outputPath := ctx.GeneratePath(".pdf")
|
outputPath := ctx.GeneratePath(".pdf")
|
||||||
|
|
||||||
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
err := chromium.Pdf(ctx, ctx.Log(), url, outputPath, options)
|
||||||
|
err = handleChromiumError(err, url, options.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
if errors.Is(err, ErrUrlNotAuthorized) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusForbidden,
|
|
||||||
fmt.Sprintf("'%s' does not match the authorized URLs", url),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
|
if errors.Is(err, ErrOmitBackgroundWithoutPrintBackground) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
err,
|
||||||
api.NewSentinelHttpError(
|
api.NewSentinelHttpError(
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
"omitBackground requires printBackground set to true",
|
"omitBackground requires printBackground set to true",
|
||||||
@@ -345,23 +520,6 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
|
||||||
if options.WaitForExpression == "" {
|
|
||||||
// We do not expect the 'waitWindowStatus' form field to return
|
|
||||||
// an ErrInvalidEvaluationExpression error. In such a scenario,
|
|
||||||
// we return a 500.
|
|
||||||
return fmt.Errorf("convert to PDF: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusBadRequest,
|
|
||||||
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidPrinterSettings) {
|
if errors.Is(err, ErrInvalidPrinterSettings) {
|
||||||
return api.WrapError(
|
return api.WrapError(
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
fmt.Errorf("convert to PDF: %w", err),
|
||||||
@@ -382,26 +540,6 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, ErrInvalidHttpStatusCode) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusConflict,
|
|
||||||
fmt.Sprintf("Invalid HTTP status code from the main page: %s", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrInvalidHttpStatusCode.Error()), "")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, ErrConsoleExceptions) {
|
|
||||||
return api.WrapError(
|
|
||||||
fmt.Errorf("convert to PDF: %w", err),
|
|
||||||
api.NewSentinelHttpError(
|
|
||||||
http.StatusConflict,
|
|
||||||
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("convert to PDF: %w", err)
|
return fmt.Errorf("convert to PDF: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,3 +578,76 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func screenshotUrl(ctx *api.Context, chromium Api, url string, options ScreenshotOptions) error {
|
||||||
|
ext := fmt.Sprintf(".%s", options.Format)
|
||||||
|
outputPath := ctx.GeneratePath(ext)
|
||||||
|
|
||||||
|
err := chromium.Screenshot(ctx, ctx.Log(), url, outputPath, options)
|
||||||
|
err = handleChromiumError(err, url, options.Options)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctx.AddOutputPaths(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleChromiumError(err error, url string, options Options) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrUrlNotAuthorized) {
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusForbidden,
|
||||||
|
fmt.Sprintf("'%s' does not match the authorized URLs", url),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrInvalidEvaluationExpression) {
|
||||||
|
if options.WaitForExpression == "" {
|
||||||
|
// We do not expect the 'waitWindowStatus' form field to return
|
||||||
|
// an ErrInvalidEvaluationExpression error. In such a scenario,
|
||||||
|
// we return a 500.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusBadRequest,
|
||||||
|
fmt.Sprintf("The expression '%s' (waitForExpression) returned an exception or undefined", options.WaitForExpression),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrInvalidHttpStatusCode) {
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusConflict,
|
||||||
|
fmt.Sprintf("Invalid HTTP status code from the main page: %s", strings.ReplaceAll(err.Error(), fmt.Sprintf(": %s", ErrInvalidHttpStatusCode.Error()), "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, ErrConsoleExceptions) {
|
||||||
|
return api.WrapError(
|
||||||
|
err,
|
||||||
|
api.NewSentinelHttpError(
|
||||||
|
http.StatusConflict,
|
||||||
|
fmt.Sprintf("Chromium console exceptions:\n %s", strings.ReplaceAll(err.Error(), ErrConsoleExceptions.Error(), "")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options) chromedp.ActionFunc {
|
func printToPdfActionFunc(logger *zap.Logger, outputPath string, options PdfOptions) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
printToPdf := page.PrintToPDF().
|
printToPdf := page.PrintToPDF().
|
||||||
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
|
WithTransferMode(page.PrintToPDFTransferModeReturnAsStream).
|
||||||
@@ -31,8 +31,8 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
WithPageRanges(options.PageRanges).
|
WithPageRanges(options.PageRanges).
|
||||||
WithPreferCSSPageSize(options.PreferCssPageSize)
|
WithPreferCSSPageSize(options.PreferCssPageSize)
|
||||||
|
|
||||||
hasCustomHeaderFooter := options.HeaderTemplate != DefaultOptions().HeaderTemplate ||
|
hasCustomHeaderFooter := options.HeaderTemplate != DefaultPdfOptions().HeaderTemplate ||
|
||||||
options.FooterTemplate != DefaultOptions().FooterTemplate
|
options.FooterTemplate != DefaultPdfOptions().FooterTemplate
|
||||||
|
|
||||||
if !hasCustomHeaderFooter {
|
if !hasCustomHeaderFooter {
|
||||||
logger.Debug("no custom header nor footer")
|
logger.Debug("no custom header nor footer")
|
||||||
@@ -63,7 +63,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := reader.Close()
|
err = reader.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("close reader: %s", err))
|
logger.Error(fmt.Sprintf("close reader: %s", err))
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := file.Close()
|
err = file.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(fmt.Sprintf("close output path: %s", err))
|
logger.Error(fmt.Sprintf("close output path: %s", err))
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,47 @@ func printToPdfActionFunc(logger *zap.Logger, outputPath string, options Options
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func captureScreenshotActionFunc(logger *zap.Logger, outputPath string, options ScreenshotOptions) chromedp.ActionFunc {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
captureScreenshot := page.CaptureScreenshot().
|
||||||
|
WithCaptureBeyondViewport(true).
|
||||||
|
WithFromSurface(true).
|
||||||
|
WithOptimizeForSpeed(options.OptimizeForSpeed).
|
||||||
|
WithFormat(page.CaptureScreenshotFormat(options.Format))
|
||||||
|
|
||||||
|
if options.Format == "jpeg" {
|
||||||
|
captureScreenshot = captureScreenshot.
|
||||||
|
WithQuality(int64(options.Quality))
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug(fmt.Sprintf("capture screenshot with: %+v", captureScreenshot))
|
||||||
|
|
||||||
|
buffer, err := captureScreenshot.Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("capture screenshot: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(fmt.Sprintf("close output path: %s", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
_, err = file.Write(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write result to output path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
|
func clearCacheActionFunc(logger *zap.Logger, clear bool) chromedp.ActionFunc {
|
||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
// See https://github.com/gotenberg/gotenberg/issues/753.
|
// See https://github.com/gotenberg/gotenberg/issues/753.
|
||||||
@@ -211,7 +252,6 @@ func hideDefaultWhiteBackgroundActionFunc(logger *zap.Logger, omitBackground, pr
|
|||||||
// See https://github.com/gotenberg/gotenberg/issues/226.
|
// See https://github.com/gotenberg/gotenberg/issues/226.
|
||||||
if !omitBackground {
|
if !omitBackground {
|
||||||
logger.Debug("default white background not hidden")
|
logger.Debug("default white background not hidden")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +310,6 @@ func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.A
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if mediaType == "" {
|
if mediaType == "" {
|
||||||
logger.Debug("no emulated media type")
|
logger.Debug("no emulated media type")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,13 +333,11 @@ func waitDelayBeforePrintActionFunc(logger *zap.Logger, disableJavaScript bool,
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if disableJavaScript {
|
if disableJavaScript {
|
||||||
logger.Debug("JavaScript disabled, skipping wait delay")
|
logger.Debug("JavaScript disabled, skipping wait delay")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if delay <= 0 {
|
if delay <= 0 {
|
||||||
logger.Debug("no wait delay")
|
logger.Debug("no wait delay")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,13 +358,11 @@ func waitForExpressionBeforePrintActionFunc(logger *zap.Logger, disableJavaScrip
|
|||||||
return func(ctx context.Context) error {
|
return func(ctx context.Context) error {
|
||||||
if disableJavaScript {
|
if disableJavaScript {
|
||||||
logger.Debug("JavaScript disabled, skipping wait expression")
|
logger.Debug("JavaScript disabled, skipping wait expression")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if expression == "" {
|
if expression == "" {
|
||||||
logger.Debug("no wait expression")
|
logger.Debug("no wait expression")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user