Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ ink
@hey-api/openapi-ts
vscode-languageserver-textdocument
yaml-language-server
@pdf-lib/upng
@atlaskit/pragmatic-drag-and-drop
@atlaskit/pragmatic-drag-and-drop-hitbox
@opentelemetry/exporter-metrics-otlp-proto
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,7 @@
"@opentelemetry/instrumentation-undici": "0.19.0",
"@opentelemetry/otlp-exporter-base": "0.208.0",
"@opentelemetry/semantic-conventions": "1.38.0",
"@pdf-lib/upng": "1.0.1",
"@reduxjs/toolkit": "1.9.7",
"@slack/webhook": "7.0.6",
"@smithy/eventstream-codec": "4.1.1",
Expand Down
1 change: 1 addition & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -2907,6 +2907,7 @@
"@types/extract-zip",
"@types/pdfmake",
"extract-zip",
"@pdf-lib/upng",
"pdfmake"
],
"reviewers": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const timezoneSchema = z.string().refine((val) => validateTimezone(val) === unde

const dimensionsSchema = z
.object({
height: z.number().positive().max(14400),
height: z.number().positive().max(32000),
width: z.number().positive().max(14400),
})
.strict();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const stubPage = {
isClosed: jest.fn(),
setViewport: jest.fn(),
evaluate: jest.fn(),
screenshot: jest.fn().mockResolvedValue(`you won't believe this one weird screenshot`),
screenshot: jest.fn().mockResolvedValue(new Uint8Array([3, 1, 4, 1, 5])),
evaluateOnNewDocument: jest.fn(),
setRequestInterception: jest.fn(),
_client: jest.fn(() => ({ on: jest.fn() })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import type { Size } from '../../../common/layout';
import { PreserveLayout } from '../../layouts/preserve_layout';
import { HeadlessChromiumDriver } from './driver';

// from __mocks__/puppeteer.ts
const SCREENSHOT_BYTES = new Uint8Array([3, 1, 4, 1, 5]);

describe('chromium driver', () => {
let mockConfig: ConfigType;
let mockLogger: Logger;
Expand Down Expand Up @@ -47,7 +50,7 @@ describe('chromium driver', () => {
};

mockPage = {
screenshot: jest.fn().mockResolvedValue(`you won't believe this one weird screenshot`),
screenshot: jest.fn().mockResolvedValue(SCREENSHOT_BYTES),
evaluate: jest.fn(),
} as unknown as puppeteer.Page;

Expand All @@ -71,14 +74,15 @@ describe('chromium driver', () => {
);

const result = await driver.screenshot({
logger: mockLogger,
elementPosition: {
boundingClientRect: { top: 200, left: 10, height: 10, width: 100 },
scroll: { x: 100, y: 300 },
},
layout: new PreserveLayout({ width: 16, height: 16 }),
});

expect(result).toEqual(Buffer.from(`you won't believe this one weird screenshot`, 'base64'));
expect(result).toEqual(Buffer.from(SCREENSHOT_BYTES));
});

it('add error to screenshot contents', async () => {
Expand All @@ -93,6 +97,7 @@ describe('chromium driver', () => {
const testSpy = jest.spyOn(driver, 'injectScreenshottingErrorHeader');

const result = await driver.screenshot({
logger: mockLogger,
elementPosition: {
boundingClientRect: { top: 200, left: 10, height: 10, width: 100 },
scroll: { x: 100, y: 300 },
Expand All @@ -107,7 +112,7 @@ describe('chromium driver', () => {
"[data-shared-items-container]",
]
`);
expect(result).toEqual(Buffer.from(`you won't believe this one weird screenshot`, 'base64'));
expect(result).toEqual(Buffer.from(SCREENSHOT_BYTES));
});

it('sets the PDF image size', async () => {
Expand All @@ -123,6 +128,7 @@ describe('chromium driver', () => {
const testSpy = jest.spyOn(layout, 'setPdfImageSize');

await driver.screenshot({
logger: mockLogger,
elementPosition: {
boundingClientRect: { top: 200, left: 10, height: 10, width: 100 },
scroll: { x: 100, y: 300 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getPrintLayoutSelectors } from '../../layouts/print_layout';
import { allowRequest } from '../network_policy';
import { stripUnsafeHeaders } from './strip_unsafe_headers';
import { getFooterTemplate, getHeaderTemplate } from './templates';

import { getTiledScreenshot } from './tiled_screenshot';
declare module 'puppeteer' {
interface Page {
_client(): CDPSession;
Expand Down Expand Up @@ -257,10 +257,12 @@ export class HeadlessChromiumDriver {
* Receive a PNG buffer of the page screenshot from Chromium
*/
public async screenshot({
logger,
elementPosition,
layout,
error,
}: {
logger: Logger;
elementPosition: ElementPosition;
layout: Layout;
error?: Error;
Expand All @@ -276,25 +278,17 @@ export class HeadlessChromiumDriver {
width: boundingClientRect.width,
});

const screenshot = await this.page.screenshot({
clip: {
return await getTiledScreenshot({
logger,
page: this.page,
rect: {
x: boundingClientRect.left + scroll.x,
y: boundingClientRect.top + scroll.y,
height: boundingClientRect.height,
width: boundingClientRect.width,
},
captureBeyondViewport: false, // workaround for an internal resize. See: https://github.com/puppeteer/puppeteer/issues/7043
zoom: layout.getBrowserZoom(),
});

if (screenshot.byteLength) {
return Buffer.from(screenshot);
}

if (typeof screenshot === 'string') {
return Buffer.from(screenshot, 'base64');
}

return undefined;
}

evaluate<A extends unknown[], T = void>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { partitionScreen, ROWS_PER_TILE } from './tiled_screenshot';

describe('stitched_screenshot', () => {
describe('partitionScreen', () => {
it('handles 0 size image', () => {
const result = partitionScreen({ x: 0, y: 0, width: 0, height: 0 });
expect(result).toMatchInlineSnapshot(`Array []`);
});

it('handles a small image', () => {
const result = partitionScreen({ x: 1, y: 2, width: 10, height: 20 });
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"height": 20,
"width": 10,
"x": 1,
"y": 2,
},
]
`);
});

const ROW_MULTIPLE = ROWS_PER_TILE * 2;

it('handles a large image, off multiple of tile size by -1', () => {
const result = partitionScreen({ x: 3, y: 4, width: 2000, height: ROW_MULTIPLE - 1 });
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"height": 1999,
"width": 2000,
"x": 3,
"y": 4,
},
]
`);
});

it('handles a large image, multiple of tile size', () => {
const result = partitionScreen({ x: 3, y: 4, width: 2000, height: ROW_MULTIPLE });
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"height": 2000,
"width": 2000,
"x": 3,
"y": 4,
},
]
`);
});

it('handles a large image, off multiple of tile size by +1', () => {
const result = partitionScreen({ x: 3, y: 4, width: 2000, height: ROW_MULTIPLE + 1 });
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"height": 2001,
"width": 2000,
"x": 3,
"y": 4,
},
]
`);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { Logger } from '@kbn/core/server';
import type { Page, BoundingBox } from 'puppeteer';
import UPNG from '@pdf-lib/upng';

export const ROWS_PER_TILE = 1000;
export const TILED_ROW_HEIGHT = 8000;

interface GetScreenshotParams {
logger: Logger;
page: Page;
rect: BoundingBox;
zoom: number;
}

export async function getTiledScreenshot(params: GetScreenshotParams): Promise<Buffer | undefined> {
try {
return getTiledScreenshotWrapped(params);
} catch (err) {
params.logger.error(`error generating screenshot: ${err.message}`, err);
return;
}
}

export async function getTiledScreenshotWrapped(
params: GetScreenshotParams
): Promise<Buffer | undefined> {
const { page, rect, zoom, logger } = params;

const tiles = partitionScreen(rect);
if (tiles.length === 0) {
logger.warn('screenshot was 0-sized, skipping');
return;
}

// disable tiling for now
if (tiles.length > 0) {
const screenshot = await getSingleScreenshot(params);
return Buffer.from(screenshot);
}

const messagePrefix = 'getTiledScreenshot: ';
const messageMeta = { tags: ['tiled-report'] };
const debug = (message: string) => logger.debug(`${messagePrefix}${message}`, messageMeta);

// 4 bytes for each pixel (RGBA), multiply by zoom (both dimensions)
const bufferSize = 4 * zoom * zoom * rect.width * rect.height;
const result = new Uint8Array(new ArrayBuffer(bufferSize));
debug(
`allocated buffer: width: ${rect.width}; height: ${rect.height}; bufferSize: ${bufferSize}`
);

let offset = 0;
for (const tile of tiles) {
debug(`getting tile: ${JSON.stringify(tile)}`);
const screenshot = await getSingleScreenshot({ logger, page, rect: tile, zoom });
const image = UPNG.decode(screenshot);
const rgbaBytes = UPNG.toRGBA8(image)[0];
debug(`got tile: width: ${image.width}; height: ${image.height}; depth: ${image.depth}`);
const imageData = new Uint8Array(rgbaBytes);

debug(`copying tile to buffer offset: ${offset}`);
result.set(imageData, offset);

offset += rgbaBytes.byteLength;
}

const finalWidth = rect.width * zoom;
const finalHeight = rect.height * zoom;
debug(`result image: width: ${finalWidth}; height: ${finalHeight}`);

const resultImage = UPNG.encode([result.buffer], finalWidth, finalHeight, 0);
return Buffer.from(resultImage);
}

async function getSingleScreenshot(params: GetScreenshotParams): Promise<ArrayBuffer> {
const { page } = params;

const image = await page.screenshot({
clip: params.rect,
captureBeyondViewport: false, // workaround for an internal resize. See: https://github.com/puppeteer/puppeteer/issues/7043
});
return image.buffer as ArrayBuffer;
}

// Split a page into tiles but only length-wise, as it's easy to
// concatenate rows, much harder to concatenate columns. And we've
// only seen issues with "big dashboards" be long ones, not wide ones.
export function partitionScreen(rect: BoundingBox): BoundingBox[] {
const result: BoundingBox[] = [];

const { x, y, width, height } = rect;
if (height <= 0) return [];

if (height <= TILED_ROW_HEIGHT) {
return [rect];
}

let currY = 0;
while (currY < height) {
const tileHeight =
currY + ROWS_PER_TILE <= height
? // use ROWS_PER_TILE if this tile has at least that many rows
ROWS_PER_TILE
: // otherwse use remaining rows
height - currY;

result.push({
x,
y: y + currY,
width,
height: tileHeight,
});

currY += ROWS_PER_TILE;
}

return result;
}
Loading
Loading