diff --git a/pkg/modules/chromium/browser.go b/pkg/modules/chromium/browser.go index b4594830..1e8f100f 100644 --- a/pkg/modules/chromium/browser.go +++ b/pkg/modules/chromium/browser.go @@ -288,7 +288,7 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *zap.Logger, url, outp navigateActionFunc(logger, url, options.SkipNetworkIdleEvent), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground), forceExactColorsActionFunc(logger, options.PrintBackground), - emulateMediaTypeActionFunc(logger, options.EmulatedMediaType), + emulateMediaTypeActionFunc(logger, options.EmulatedMediaType, options.EmulatedMediaFeatures), waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression), waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector), waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay), @@ -314,7 +314,7 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *zap.Logger, ur navigateActionFunc(logger, url, options.SkipNetworkIdleEvent), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true), forceExactColorsActionFunc(logger, true), - emulateMediaTypeActionFunc(logger, options.EmulatedMediaType), + emulateMediaTypeActionFunc(logger, options.EmulatedMediaType, options.EmulatedMediaFeatures), waitForExpressionBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitForExpression), waitForSelectorVisibleBeforePrintActionFunc(logger, options.WaitForSelector), waitDelayBeforePrintActionFunc(logger, b.arguments.disableJavaScript, options.WaitDelay), diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index 2645eefb..56197565 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -165,11 +165,28 @@ type Options struct { // "print". EmulatedMediaType string + // EmulatedMediaFeatures are the media features to emulate, e.g., + // [{"name": "prefers-color-scheme", "value": "dark"}]. + EmulatedMediaFeatures []EmulatedMediaFeature + // OmitBackground hides the default white background and allows generating // PDFs with transparency. OmitBackground bool } +// EmulatedMediaFeature gathers the available entries for emulating a media +// feature. +type EmulatedMediaFeature struct { + // Name is the media feature name (e.g., "prefers-color-scheme", + // "prefers-reduced-motion"). + // Required. + Name string `json:"name"` + + // Value is the media feature value (e.g., "dark", "reduce"). + // Required. + Value string `json:"value"` +} + // DefaultOptions returns the default values for Options. func DefaultOptions() Options { return Options{ @@ -187,6 +204,7 @@ func DefaultOptions() Options { UserAgent: "", ExtraHttpHeaders: nil, EmulatedMediaType: "", + EmulatedMediaFeatures: nil, OmitBackground: false, } } diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 6398594b..d3ec73d4 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -39,6 +39,7 @@ var sameSiteRegexp = regexp2.MustCompile( // - ignoreResourceHttpStatusDomains: []string // - cookies: []Cookie // - extraHttpHeaders: map[string]string +// - emulatedMediaFeatures: map[string]string // // Domain filtering only applies to resource checks triggered by // "failOnResourceHttpStatusCodes". @@ -60,6 +61,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { userAgent string extraHttpHeaders []ExtraHttpHeader emulatedMediaType string + emulatedMediaFeatures []EmulatedMediaFeature omitBackground bool ) @@ -227,6 +229,27 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { return nil }). + Custom("emulatedMediaFeatures", func(value string) error { + if value == "" { + emulatedMediaFeatures = defaultOptions.EmulatedMediaFeatures + return nil + } + + var features map[string]string + err := json.Unmarshal([]byte(value), &features) + if err != nil { + return fmt.Errorf("unmarshal emulatedMediaFeatures: %w", err) + } + + for k, v := range features { + emulatedMediaFeatures = append(emulatedMediaFeatures, EmulatedMediaFeature{ + Name: k, + Value: v, + }) + } + + return err + }). Bool("omitBackground", &omitBackground, defaultOptions.OmitBackground) options := Options{ @@ -244,6 +267,7 @@ func FormDataChromiumOptions(ctx *api.Context) (*api.FormData, Options) { UserAgent: userAgent, ExtraHttpHeaders: extraHttpHeaders, EmulatedMediaType: emulatedMediaType, + EmulatedMediaFeatures: emulatedMediaFeatures, OmitBackground: omitBackground, } diff --git a/pkg/modules/chromium/tasks.go b/pkg/modules/chromium/tasks.go index 1169d9fa..a6df949e 100644 --- a/pkg/modules/chromium/tasks.go +++ b/pkg/modules/chromium/tasks.go @@ -423,26 +423,44 @@ func forceExactColorsActionFunc(logger *zap.Logger, printBackground bool) chrome } } -func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string) chromedp.ActionFunc { +func emulateMediaTypeActionFunc(logger *zap.Logger, mediaType string, mediaFeatures []EmulatedMediaFeature) chromedp.ActionFunc { return func(ctx context.Context) error { - if mediaType == "" { - logger.Debug("no emulated media type") + if mediaType == "" && len(mediaFeatures) == 0 { + logger.Debug("no emulated media type or features") return nil } - if mediaType != "screen" && mediaType != "print" { + if mediaType != "" && mediaType != "screen" && mediaType != "print" { return fmt.Errorf("validate emulated media type '%s': %w", mediaType, ErrInvalidEmulatedMediaType) } - logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType)) - emulatedMedia := emulation.SetEmulatedMedia() - err := emulatedMedia.WithMedia(mediaType).Do(ctx) + + if mediaType != "" { + logger.Debug(fmt.Sprintf("emulate media type '%s'", mediaType)) + emulatedMedia = emulatedMedia.WithMedia(mediaType) + } + + if len(mediaFeatures) > 0 { + logger.Debug(fmt.Sprintf("emulate media features %+v", mediaFeatures)) + + features := make([]*emulation.MediaFeature, len(mediaFeatures)) + for i, f := range mediaFeatures { + features[i] = &emulation.MediaFeature{ + Name: f.Name, + Value: f.Value, + } + } + + emulatedMedia = emulatedMedia.WithFeatures(features) + } + + err := emulatedMedia.Do(ctx) if err == nil { return nil } - return fmt.Errorf("emulate media type '%s': %w", mediaType, err) + return fmt.Errorf("emulate media: %w", err) } } diff --git a/test/integration/features/chromium_convert_html.feature b/test/integration/features/chromium_convert_html.feature index 876446c8..899df667 100644 --- a/test/integration/features/chromium_convert_html.feature +++ b/test/integration/features/chromium_convert_html.feature @@ -281,6 +281,82 @@ Feature: /forms/chromium/convert/html Emulated media type is 'print'. """ + Scenario: POST /forms/chromium/convert/html (Emulated Media Features) + Given I have a default Gotenberg container + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Prefers reduced motion. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Prefers reduced motion. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | emulatedMediaType | screen | field | + | emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Prefers reduced motion. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'print'. + """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/feature-rich-html/index.html | file | + | emulatedMediaType | print | field | + | emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Prefers reduced motion. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Scenario: POST /forms/chromium/convert/html (Default Allow / Deny Lists) Given I have a default Gotenberg container When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): @@ -550,6 +626,15 @@ Feature: /forms/chromium/convert/html """ Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`) """ + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): + | files | testdata/page-1-html/index.html | file | + | emulatedMediaFeatures | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'emulatedMediaFeatures' is invalid (got 'foo', resulting to unmarshal emulatedMediaFeatures: invalid character 'o' in literal false (expecting 'a')) + """ When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s): | files | testdata/page-1-html/index.html | file | | splitMode | foo | field | diff --git a/test/integration/features/chromium_convert_url.feature b/test/integration/features/chromium_convert_url.feature index ae6fb88e..7907d108 100644 --- a/test/integration/features/chromium_convert_url.feature +++ b/test/integration/features/chromium_convert_url.feature @@ -346,6 +346,86 @@ Feature: /forms/chromium/convert/url Emulated media type is 'print'. """ + Scenario: POST /forms/chromium/convert/url (Emulated Media Features) + Given I have a default Gotenberg container + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Prefers reduced motion. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Prefers reduced motion. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | emulatedMediaType | screen | field | + | emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Prefers reduced motion. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/feature-rich-html-remote/index.html | field | + | emulatedMediaType | print | field | + | emulatedMediaFeatures | {"prefers-reduced-motion":"reduce"} | field | + | Gotenberg-Output-Filename | foo | header | + Then the response status code should be 200 + Then the response header "Content-Type" should be "application/pdf" + Then there should be 1 PDF(s) in the response + Then there should be the following file(s) in the response: + | foo.pdf | + Then the "foo.pdf" PDF should have 1 page(s) + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Emulated media type is 'print'. + """ + Then the "foo.pdf" PDF should have the following content at page 1: + """ + Prefers reduced motion. + """ + Then the "foo.pdf" PDF should NOT have the following content at page 1: + """ + Emulated media type is 'screen'. + """ + Scenario: POST /forms/chromium/convert/url (Default Allow / Deny Lists) Given I have a default Gotenberg container Given I have a static server @@ -627,6 +707,16 @@ Feature: /forms/chromium/convert/url Invalid form data: form field 'extraHttpHeaders' is invalid (got '{"foo":"bar;scope=*."}', resulting to invalid scope regex pattern for header 'foo': error parsing regexp: missing argument to repetition operator in `*.`) """ Given I have a static server + When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): + | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | + | emulatedMediaFeatures | foo | field | + Then the response status code should be 400 + Then the response header "Content-Type" should be "text/plain; charset=UTF-8" + Then the response body should match string: + """ + Invalid form data: form field 'emulatedMediaFeatures' is invalid (got 'foo', resulting to unmarshal emulatedMediaFeatures: invalid character 'o' in literal false (expecting 'a')) + """ + Given I have a static server When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/url" endpoint with the following form data and header(s): | url | http://host.docker.internal:%d/html/testdata/page-1-html/index.html | field | | splitMode | foo | field | diff --git a/test/integration/testdata/feature-rich-html-remote/index.html b/test/integration/testdata/feature-rich-html-remote/index.html index f7183740..af1b006a 100644 --- a/test/integration/testdata/feature-rich-html-remote/index.html +++ b/test/integration/testdata/feature-rich-html-remote/index.html @@ -20,6 +20,14 @@ display: none; } } + #reduced-motion { + display: none; + } + @media (prefers-reduced-motion: reduce) { + #reduced-motion { + display: block; + } + } @@ -29,6 +37,7 @@

Emulated media type is 'print'.

Emulated media type is 'screen'.

+

Prefers reduced motion.

diff --git a/test/integration/testdata/feature-rich-html/index.html b/test/integration/testdata/feature-rich-html/index.html index dc1537d4..e2aee534 100644 --- a/test/integration/testdata/feature-rich-html/index.html +++ b/test/integration/testdata/feature-rich-html/index.html @@ -28,6 +28,14 @@ display: none; } } + #reduced-motion { + display: none; + } + @media (prefers-reduced-motion: reduce) { + #reduced-motion { + display: block; + } + } @@ -37,6 +45,7 @@

Emulated media type is 'print'.

Emulated media type is 'screen'.

+

Prefers reduced motion.