Skip to content

Commit d2b2271

Browse files
Merge pull request #1207 from gemini-testing/TESTPLANE-896.base64_size_parse
chore: get rid of extra base64 png parsing
2 parents 22b6508 + a2bb56a commit d2b2271

4 files changed

Lines changed: 72 additions & 10 deletions

File tree

src/image.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import looksSame from "looks-same";
33
import { loadEsm } from "./utils/preload-utils";
44
import { DiffOptions, ImageSize } from "./types";
55
import { convertRgbaToPng } from "./utils/eight-bit-rgba-to-png";
6-
import { BITS_IN_BYTE, PNG_HEIGHT_OFFSET, PNG_WIDTH_OFFSET, RGBA_CHANNELS } from "./constants/png";
6+
import {
7+
BITS_IN_BYTE,
8+
PNG_HEIGHT_OFFSET,
9+
PNG_MIN_ASSIST_BYTES,
10+
PNG_SIGNATURE,
11+
PNG_WIDTH_OFFSET,
12+
RGBA_CHANNELS,
13+
} from "./constants/png";
714

815
interface PngImageData {
916
data: Buffer;
@@ -49,6 +56,30 @@ const jsquashDecode = (buffer: ArrayBuffer): Promise<ImageData> => {
4956
]).then(([mod]) => mod.decode(buffer, { bitDepth: BITS_IN_BYTE }));
5057
};
5158

59+
export const extractBase64PngSize = (base64EncodedPng: string): ImageSize => {
60+
// Strips data URI prefix if it exists
61+
const base64Data = base64EncodedPng.includes(";base64,")
62+
? (base64EncodedPng.split(";base64,").pop() as string)
63+
: base64EncodedPng;
64+
65+
if (base64Data.length <= PNG_MIN_ASSIST_BYTES) {
66+
throw new Error("Invalid base64 encoded png: too short");
67+
}
68+
69+
const headerBytesToRead = Math.max(PNG_WIDTH_OFFSET, PNG_HEIGHT_OFFSET) + 4;
70+
const headerCharsToRead = Math.ceil(headerBytesToRead / 3) * 4;
71+
const pngHeader = Buffer.from(base64Data.slice(0, headerCharsToRead), "base64");
72+
73+
if (!pngHeader.subarray(0, PNG_SIGNATURE.byteLength).equals(PNG_SIGNATURE)) {
74+
throw new Error("Invalid base64 encoded png: signature missmatch");
75+
}
76+
77+
return {
78+
width: pngHeader.readUInt32BE(PNG_WIDTH_OFFSET),
79+
height: pngHeader.readUInt32BE(PNG_HEIGHT_OFFSET),
80+
};
81+
};
82+
5283
export class Image {
5384
private _imgDataPromise: Promise<Buffer>;
5485
private _imgData: Buffer | null = null;

src/worker/runner/test-runner/one-time-screenshooter.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use strict";
22

3-
const { Image } = require("../../../image");
3+
const { extractBase64PngSize } = require("../../../image");
44
const ScreenShooter = require("../../../browser/screen-shooter");
55
const logger = require("../../../utils/logger");
66
const { promiseTimeout } = require("../../../utils/promise");
@@ -106,8 +106,7 @@ module.exports = class OneTimeScreenshooter {
106106

107107
async _makeViewportScreenshot() {
108108
const base64 = await this._browser.publicAPI.takeScreenshot();
109-
const image = Image.fromBase64(base64);
110-
const size = await image.getSize();
109+
const size = extractBase64PngSize(base64);
111110

112111
return { base64, size };
113112
}

test/src/image.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"use strict";
22

33
const proxyquire = require("proxyquire");
4+
const { extractBase64PngSize } = require("src/image");
45

56
describe("Image", () => {
67
const sandbox = sinon.createSandbox();
@@ -53,6 +54,36 @@ describe("Image", () => {
5354

5455
afterEach(() => sandbox.restore());
5556

57+
describe("extractBase64PngSize", () => {
58+
it("should throw error on invalid small strings", () => {
59+
const fn = () => extractBase64PngSize("foobar");
60+
61+
assert.throw(fn, "Invalid base64 encoded png: too short");
62+
});
63+
64+
it("should throw error on non-base64 png strings", () => {
65+
const fn = () => extractBase64PngSize("foobar".repeat(20));
66+
67+
assert.throw(fn, "Invalid base64 encoded png: signature missmatch");
68+
});
69+
70+
it("should work with minimal png", () => {
71+
const minimalPng =
72+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQImWNgYGAAAAAEAAGjChXjAAAAAElFTkSuQmCC";
73+
const result = extractBase64PngSize(minimalPng);
74+
75+
assert.deepEqual(result, { width: 1, height: 1 });
76+
});
77+
78+
it("should extract size", () => {
79+
const tenPxSquarePng =
80+
"iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC";
81+
const result = extractBase64PngSize(tenPxSquarePng);
82+
83+
assert.deepEqual(result, { width: 10, height: 10 });
84+
});
85+
});
86+
5687
describe("constructor", () => {
5788
it("should read width and height from PNG buffer", () => {
5889
const buffer = createMockPngBuffer(200, 150);

test/src/worker/runner/test-runner/one-time-screenshooter.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => {
1111
const sandbox = sinon.createSandbox();
1212
let OneTimeScreenshooter;
1313
let logger;
14+
let extractBase64PngSize;
1415

1516
const mkBrowser_ = (opts = {}) => {
1617
const session = mkSessionStub_();
@@ -57,8 +58,10 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => {
5758
logger = {
5859
warn: sinon.stub(),
5960
};
61+
extractBase64PngSize = sinon.stub().named("extractBase64PngSize").returns({ width: 100500, height: 500100 });
6062
OneTimeScreenshooter = proxyquire("src/worker/runner/test-runner/one-time-screenshooter", {
6163
"../../../utils/logger": logger,
64+
"../../../image": { extractBase64PngSize },
6265
});
6366

6467
sandbox.stub(ScreenShooter.prototype, "capture").resolves(stubImage_());
@@ -71,8 +74,7 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => {
7174
it('should capture viewport screenshot if option "takeScreenshotOnFailsMode" is not set', async () => {
7275
const browser = mkBrowser_();
7376
browser.publicAPI.takeScreenshot.resolves("base64");
74-
const imgStub = stubImage_({ width: 100, height: 500 });
75-
Image.fromBase64.returns(imgStub);
77+
extractBase64PngSize.withArgs("base64").returns({ width: 100, height: 500 });
7678
const screenshooter = mkScreenshooter_({ browser });
7779

7880
await screenshooter[method](...getArgs());
@@ -86,8 +88,7 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => {
8688
it('should capture viewport screenshot if option "takeScreenshotOnFailsMode" is set to "viewport"', async () => {
8789
const browser = mkBrowser_();
8890
browser.publicAPI.takeScreenshot.resolves("base64");
89-
const imgStub = stubImage_({ width: 100, height: 500 });
90-
Image.fromBase64.returns(imgStub);
91+
extractBase64PngSize.withArgs("base64").returns({ width: 100, height: 500 });
9192
const config = { takeScreenshotOnFailsMode: "viewport" };
9293
const screenshooter = mkScreenshooter_({ browser, config });
9394

@@ -213,8 +214,8 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => {
213214
it("should extend passed error with screenshot data", async () => {
214215
const browser = mkBrowser_();
215216
browser.publicAPI.takeScreenshot.resolves("base64");
217+
extractBase64PngSize.withArgs("base64").returns({ width: 100, height: 200 });
216218
const screenshooter = mkScreenshooter_({ browser });
217-
Image.fromBase64.withArgs("base64").returns(stubImage_({ width: 100, height: 200 }));
218219

219220
const error = await screenshooter.extendWithScreenshot(new Error());
220221

@@ -286,7 +287,7 @@ describe("worker/runner/test-runner/one-time-screenshooter", () => {
286287

287288
describe("getScreenshot", () => {
288289
it("should return captured screenshot", async () => {
289-
Image.fromBase64.returns(stubImage_({ width: 100, height: 200 }));
290+
extractBase64PngSize.returns({ width: 100, height: 200 });
290291

291292
const screenshooter = mkScreenshooter_({});
292293

0 commit comments

Comments
 (0)