Skip to content

Commit 28d8093

Browse files
chore: get rid of extra base64 png parsing
1 parent 03a5228 commit 28d8093

File tree

4 files changed

+70
-10
lines changed

4 files changed

+70
-10
lines changed

src/image.ts

Lines changed: 30 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,28 @@ const jsquashDecode = (buffer: ArrayBuffer): Promise<ImageData> => {
4956
]).then(([mod]) => mod.decode(buffer, { bitDepth: BITS_IN_BYTE }));
5057
};
5158

59+
export const extractBase64PngSize = (base64EncodedString: string): ImageSize => {
60+
// Each 6 bits sequence encoded with 1 base64 char
61+
const bytesToBase64CharsRatio = 8 / 6;
62+
63+
if (base64EncodedString.length <= PNG_MIN_ASSIST_BYTES * bytesToBase64CharsRatio) {
64+
throw new Error("Invalid base64 encoded png: too short");
65+
}
66+
67+
const headerBytesToRead = Math.max(PNG_WIDTH_OFFSET, PNG_HEIGHT_OFFSET) + 4;
68+
const headerCharsToRead = Math.ceil(headerBytesToRead * bytesToBase64CharsRatio);
69+
const pngHeader = Buffer.from(base64EncodedString.slice(0, headerCharsToRead), "base64");
70+
71+
if (!pngHeader.subarray(0, PNG_SIGNATURE.byteLength).equals(PNG_SIGNATURE)) {
72+
throw new Error("Invalid base64 encoded png: signature missmatch");
73+
}
74+
75+
return {
76+
width: pngHeader.readUInt32BE(PNG_WIDTH_OFFSET),
77+
height: pngHeader.readUInt32BE(PNG_HEIGHT_OFFSET),
78+
};
79+
};
80+
5281
export class Image {
5382
private _imgDataPromise: Promise<Buffer>;
5483
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)