refactor(chromium): drop paint-callback polyfill now that chromedp is pinned

This commit is contained in:
Julien Neuhart
2026-04-30 14:05:03 +02:00
parent ace379a92c
commit 45b9f32351
3 changed files with 2 additions and 145 deletions

View File

@@ -317,7 +317,6 @@ func (b *chromiumBrowser) pdf(ctx context.Context, logger *slog.Logger, url, out
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies), setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent), userAgentOverride(logger, options.UserAgent),
injectPaintCallbacksPolyfillActionFunc(logger),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, options.PrintBackground),
forceExactColorsActionFunc(logger, options.PrintBackground), forceExactColorsActionFunc(logger, options.PrintBackground),
@@ -344,7 +343,6 @@ func (b *chromiumBrowser) screenshot(ctx context.Context, logger *slog.Logger, u
disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript), disableJavaScriptActionFunc(logger, b.arguments.disableJavaScript),
setCookiesActionFunc(logger, options.Cookies), setCookiesActionFunc(logger, options.Cookies),
userAgentOverride(logger, options.UserAgent), userAgentOverride(logger, options.UserAgent),
injectPaintCallbacksPolyfillActionFunc(logger),
navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent), navigateActionFunc(logger, url, options.SkipNetworkIdleEvent, options.SkipNetworkAlmostIdleEvent),
hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true), hideDefaultWhiteBackgroundActionFunc(logger, options.OmitBackground, true),
forceExactColorsActionFunc(logger, true), forceExactColorsActionFunc(logger, true),

View File

@@ -1,141 +0,0 @@
package chromium
import (
"context"
"fmt"
"log/slog"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
// paintCallbacksPolyfill is a JavaScript shim installed before any user
// script runs. Replaces requestAnimationFrame, cancelAnimationFrame,
// ResizeObserver, and IntersectionObserver with timer-backed
// implementations. Headless Chromium's print-emulation pipeline does
// not tick the compositor refresh driver between the load event and
// the Page.printToPDF call, which leaves the native callback queues
// stalled and breaks charting libraries (visx, ApexCharts) that rely
// on rAF-gated measurement. The polyfill exposes the same APIs with
// timer semantics so user-scheduled callbacks fire in print mode and
// the rendered output matches a live browser. See
// https://github.com/gotenberg/gotenberg/issues/1535.
const paintCallbacksPolyfill = `
(function () {
var nextHandle = 0;
var pending = new Map();
window.requestAnimationFrame = function (callback) {
var handle = ++nextHandle;
pending.set(handle, setTimeout(function () {
pending.delete(handle);
callback(performance.now());
}, 16));
return handle;
};
window.cancelAnimationFrame = function (handle) {
clearTimeout(pending.get(handle));
pending.delete(handle);
};
function PollingResizeObserver(callback) {
this._callback = callback;
this._observed = [];
this._timer = null;
}
PollingResizeObserver.prototype.observe = function (el) {
var entry = { target: el, lastW: -1, lastH: -1 };
this._observed.push(entry);
if (!this._timer) {
var self = this;
this._timer = setInterval(function () { self._tick(); }, 100);
}
this._tick();
};
PollingResizeObserver.prototype.unobserve = function (el) {
this._observed = this._observed.filter(function (e) { return e.target !== el; });
if (this._observed.length === 0 && this._timer) {
clearInterval(this._timer);
this._timer = null;
}
};
PollingResizeObserver.prototype.disconnect = function () {
this._observed = [];
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
};
PollingResizeObserver.prototype._tick = function () {
var changed = [];
for (var i = 0; i < this._observed.length; i++) {
var e = this._observed[i];
var w = e.target.offsetWidth;
var h = e.target.offsetHeight;
if (w !== e.lastW || h !== e.lastH) {
e.lastW = w;
e.lastH = h;
changed.push({
target: e.target,
contentRect: { width: w, height: h, top: 0, left: 0, right: w, bottom: h, x: 0, y: 0 },
borderBoxSize: [{ inlineSize: w, blockSize: h }],
contentBoxSize: [{ inlineSize: w, blockSize: h }],
devicePixelContentBoxSize: [{ inlineSize: w, blockSize: h }],
});
}
}
if (changed.length > 0) {
try { this._callback(changed, this); } catch (err) { console.error(err); }
}
};
window.ResizeObserver = PollingResizeObserver;
function ImmediateIntersectionObserver(callback) {
this._callback = callback;
this._observed = [];
}
ImmediateIntersectionObserver.prototype.observe = function (el) {
this._observed.push(el);
var self = this;
setTimeout(function () {
var rect = el.getBoundingClientRect();
var entry = {
target: el,
isIntersecting: true,
intersectionRatio: 1,
boundingClientRect: rect,
intersectionRect: rect,
rootBounds: null,
time: performance.now(),
};
try { self._callback([entry], self); } catch (err) { console.error(err); }
}, 0);
};
ImmediateIntersectionObserver.prototype.unobserve = function (el) {
this._observed = this._observed.filter(function (x) { return x !== el; });
};
ImmediateIntersectionObserver.prototype.disconnect = function () {
this._observed = [];
};
ImmediateIntersectionObserver.prototype.takeRecords = function () { return []; };
window.IntersectionObserver = ImmediateIntersectionObserver;
})();
`
// injectPaintCallbacksPolyfillActionFunc installs the
// [paintCallbacksPolyfill] via [page.AddScriptToEvaluateOnNewDocument]
// before any user script runs. Runs on every conversion: Chromium's
// headless-print pipeline does not fire the native rAF,
// ResizeObserver, and IntersectionObserver, so the timer-backed
// replacements fire the user's callbacks when the page relies on them
// and have no effect otherwise.
func injectPaintCallbacksPolyfillActionFunc(logger *slog.Logger) chromedp.ActionFunc {
return func(ctx context.Context) error {
logger.DebugContext(ctx, "inject paint-callbacks polyfill")
_, err := page.AddScriptToEvaluateOnNewDocument(paintCallbacksPolyfill).Do(ctx)
if err != nil {
return fmt.Errorf("inject paint-callbacks polyfill: %w", err)
}
return nil
}
}

View File

@@ -195,7 +195,7 @@ Feature: /forms/chromium/convert/html
Wait delay > 2 seconds or expression window globalVar === 'ready' returns true. Wait delay > 2 seconds or expression window globalVar === 'ready' returns true.
""" """
Scenario: POST /forms/chromium/convert/html (paint-callback polyfill fires rAF / ResizeObserver / IntersectionObserver with waitForExpression) Scenario: POST /forms/chromium/convert/html (rAF / ResizeObserver / IntersectionObserver fire with waitForExpression)
Given I have a default Gotenberg container 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): 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/paint-callbacks-html/index.html | file | | files | testdata/paint-callbacks-html/index.html | file |
@@ -219,7 +219,7 @@ Feature: /forms/chromium/convert/html
io-fired io-fired
""" """
Scenario: POST /forms/chromium/convert/html (paint-callback polyfill fires rAF / ResizeObserver / IntersectionObserver with waitDelay and emulatedMediaType=print) Scenario: POST /forms/chromium/convert/html (rAF / ResizeObserver / IntersectionObserver fire with waitDelay and emulatedMediaType=print)
Given I have a default Gotenberg container 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): 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/paint-callbacks-html/index.html | file | | files | testdata/paint-callbacks-html/index.html | file |