Skip to content

Commit 1d0a7a1

Browse files
authored
fix(go): render full height images properly (#780)
1 parent f7d5059 commit 1d0a7a1

File tree

4 files changed

+93
-5
lines changed

4 files changed

+93
-5
lines changed

pkg/service/browser.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,8 @@ func (s *BrowserService) Render(ctx context.Context, url string, optionFuncs ...
326326
scrollForElements(opts.timeBetweenScrolls),
327327
waitForDuration(time.Second),
328328
waitForReady(browserCtx, opts.timeout),
329+
resizeViewportForFullHeight(opts), // Resize after all content is loaded and ready
330+
waitForReady(browserCtx, opts.timeout), // Wait for readiness again after viewport resize
329331
opts.printer.action(fileChan, opts),
330332
}
331333
span.AddEvent("actions created")
@@ -728,7 +730,7 @@ type pngPrinter struct {
728730
fullHeight bool
729731
}
730732

731-
func (p *pngPrinter) action(dst chan []byte, _ *renderingOptions) chromedp.Action {
733+
func (p *pngPrinter) action(dst chan []byte, opts *renderingOptions) chromedp.Action {
732734
return chromedp.ActionFunc(func(ctx context.Context) error {
733735
tracer := tracer(ctx)
734736
ctx, span := tracer.Start(ctx, "pngPrinter.action",
@@ -739,7 +741,10 @@ func (p *pngPrinter) action(dst chan []byte, _ *renderingOptions) chromedp.Actio
739741

740742
output, err := page.CaptureScreenshot().
741743
WithFormat(page.CaptureScreenshotFormatPng).
742-
WithCaptureBeyondViewport(p.fullHeight).
744+
// We don't want to use this option: it doesn't take a full window screenshot,
745+
// rather it takes a screenshot including content that bleeds outside the viewport (e.g. something 110vh tall).
746+
// Instead, we change the viewport height to match the content height.
747+
WithCaptureBeyondViewport(false).
743748
Do(ctx)
744749
if err != nil {
745750
span.SetStatus(codes.Error, err.Error())
@@ -826,6 +831,54 @@ func setCookies(cookies []*network.SetCookieParams) chromedp.Action {
826831
})
827832
}
828833

834+
func resizeViewportForFullHeight(opts *renderingOptions) chromedp.Action {
835+
return chromedp.ActionFunc(func(ctx context.Context) error {
836+
// Only resize for PNG printers with fullHeight enabled
837+
pngPrinter, ok := opts.printer.(*pngPrinter)
838+
if !ok || !pngPrinter.fullHeight {
839+
return nil // Skip for non-PNG or non-fullHeight screenshots
840+
}
841+
842+
tracer := tracer(ctx)
843+
ctx, span := tracer.Start(ctx, "resizeViewportForFullHeight")
844+
defer span.End()
845+
846+
var scrollHeight int
847+
err := chromedp.Evaluate(`document.body.scrollHeight`, &scrollHeight).Do(ctx)
848+
if err != nil {
849+
span.SetStatus(codes.Error, "failed to get scroll height: "+err.Error())
850+
return fmt.Errorf("failed to get scroll height: %w", err)
851+
}
852+
853+
// Only resize if the page is actually taller than the current viewport
854+
if scrollHeight > opts.viewportHeight {
855+
span.AddEvent("resizing viewport for full height capture",
856+
trace.WithAttributes(
857+
attribute.Int("originalHeight", opts.viewportHeight),
858+
attribute.Int("newHeight", scrollHeight),
859+
))
860+
861+
// Determine orientation from options
862+
orientation := chromedp.EmulatePortrait
863+
if opts.landscape {
864+
orientation = chromedp.EmulateLandscape
865+
}
866+
867+
err = chromedp.EmulateViewport(int64(opts.viewportWidth), int64(scrollHeight), orientation).Do(ctx)
868+
if err != nil {
869+
span.SetStatus(codes.Error, "failed to resize viewport: "+err.Error())
870+
return fmt.Errorf("failed to resize viewport for full height: %w", err)
871+
}
872+
873+
span.SetStatus(codes.Ok, "viewport resized successfully")
874+
} else {
875+
span.AddEvent("no viewport resize needed", trace.WithAttributes(attribute.Int("pageHeight", scrollHeight)))
876+
}
877+
878+
return nil
879+
})
880+
}
881+
829882
func scrollForElements(timeBetweenScrolls time.Duration) chromedp.Action {
830883
return chromedp.ActionFunc(func(ctx context.Context) error {
831884
tracer := tracer(ctx)
136 KB
Loading
136 KB
Loading

tests/acceptance/rendering_grafana_test.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ func TestRenderingGrafana(t *testing.T) {
290290
}
291291
})
292292

293-
t.Run("render very long prometheus dashboard as PDF", func(t *testing.T) {
293+
t.Run("render very long prometheus dashboard", func(t *testing.T) {
294294
t.Parallel()
295295

296296
net, err := network.New(t.Context())
@@ -305,7 +305,7 @@ func TestRenderingGrafana(t *testing.T) {
305305
WithEnv("GF_RENDERING_CALLBACK_URL", "http://grafana:3000/"),
306306
WithEnv("GF_RENDERING_RENDERER_TOKEN", rendererAuthToken))
307307

308-
t.Run("render many pages", func(t *testing.T) {
308+
t.Run("render PDF of many pages", func(t *testing.T) {
309309
t.Parallel()
310310

311311
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
@@ -340,7 +340,7 @@ func TestRenderingGrafana(t *testing.T) {
340340
"first 3": "1-3",
341341
"1 and 3": "1, 3",
342342
} {
343-
t.Run("print with pageRanges="+name, func(t *testing.T) {
343+
t.Run("print PDF with pageRanges="+name, func(t *testing.T) {
344344
t.Parallel()
345345

346346
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
@@ -368,6 +368,41 @@ func TestRenderingGrafana(t *testing.T) {
368368
}
369369
})
370370
}
371+
372+
t.Run("render many pages as PNG with full height", func(t *testing.T) {
373+
t.Parallel()
374+
375+
for _, isLandscape := range []bool{true, false} {
376+
t.Run("landscape="+fmt.Sprintf("%v", isLandscape), func(t *testing.T) {
377+
t.Parallel()
378+
379+
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, svc.HTTPEndpoint+"/render", nil)
380+
require.NoError(t, err, "could not construct HTTP request to Grafana")
381+
req.Header.Set("Accept", "image/png")
382+
req.Header.Set("X-Auth-Token", "-")
383+
query := req.URL.Query()
384+
query.Set("url", "http://grafana:3000/d/very-long-prometheus-dashboard?render=1&from=1699333200000&to=1699344000000&kiosk=true")
385+
query.Set("encoding", "png")
386+
query.Set("renderKey", renderKey)
387+
query.Set("domain", "grafana")
388+
query.Set("height", "-1")
389+
query.Set("landscape", fmt.Sprintf("%v", isLandscape))
390+
req.URL.RawQuery = query.Encode()
391+
392+
resp, err := http.DefaultClient.Do(req)
393+
require.NoError(t, err, "could not send HTTP request to Grafana")
394+
require.Equal(t, http.StatusOK, resp.StatusCode, "unexpected HTTP status code from Grafana")
395+
396+
body := ReadBody(t, resp.Body)
397+
image := ReadRGBA(t, body)
398+
fixture := fmt.Sprintf("render-very-long-prometheus-dashboard-full-height-landscape-%v.png", isLandscape)
399+
fixtureImg := ReadFixtureRGBA(t, fixture)
400+
if !AssertPixelDifference(t, fixtureImg, image, 125_000) { // this is a very long image, so data may be off by a little bit
401+
UpdateFixtureIfEnabled(t, fixture, body)
402+
}
403+
})
404+
}
405+
})
371406
})
372407

373408
t.Run("render panel dashboards as PNG", func(t *testing.T) {

0 commit comments

Comments
 (0)