Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save images offline, in the snapshot #770

Merged
merged 6 commits into from
Jan 11, 2022
Merged
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
1 change: 1 addition & 0 deletions guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ The parameter of `rrweb.record` accepts the following options.
| packFn | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
| sampling | - | refer to the [storage optimization recipe](./docs/recipes/optimize-storage.md) |
| recordCanvas | false | whether to record the canvas element |
| inlineImages | false | whether to record the image content |
| collectFonts | false | whether to collect fonts in the website |
| recordLog | false | whether to record console output, refer to the [console recipe](./docs/recipes/console.md) |
| userTriggeredOnInput | false | whether to add `userTriggered` on input events that indicates if this event was triggered directly by the user or not. [What is `userTriggered`?](https://github.com/rrweb-io/rrweb/pull/495) |
Expand Down
18 changes: 12 additions & 6 deletions packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,18 +226,24 @@ function buildNode(
ctx.drawImage(image, 0, 0, image.width, image.height);
}
};
} else if (tagName === 'img' && name === 'rr_dataURL') {
const image = (node as HTMLImageElement);
if (!image.currentSrc.startsWith('data:')) {
// backup original img src
image.setAttribute('data-rrweb-src', image.currentSrc);
image.src = value;
}
image.removeAttribute(name);
}

if (name === 'rr_width') {
(node as HTMLElement).style.width = value;
}
if (name === 'rr_height') {
} else if (name === 'rr_height') {
(node as HTMLElement).style.height = value;
}
if (name === 'rr_mediaCurrentTime') {
} else if (name === 'rr_mediaCurrentTime') {
(node as HTMLMediaElement).currentTime = n.attributes
.rr_mediaCurrentTime as number;
}
if (name === 'rr_mediaState') {
} else if (name === 'rr_mediaState') {
switch (value) {
case 'played':
(node as HTMLMediaElement)
Expand Down
40 changes: 40 additions & 0 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ function extractOrigin(url: string): string {
return origin;
}

let canvasService: HTMLCanvasElement | null;
let canvasCtx: CanvasRenderingContext2D | null;

function initCanvasService(doc: Document) {
if (!canvasService) {
canvasService = doc.createElement('canvas');
}
if (!canvasCtx) {
canvasCtx = canvasService.getContext('2d');
}
canvasService.width = 0;
canvasService.height = 0;
}

const URL_IN_CSS_REF = /url\((?:(')([^']*)'|(")(.*?)"|([^)]*))\)/gm;
const RELATIVE_PATH = /^(?!www\.|(?:http|ftp)s?:\/\/|[A-Za-z]:\\|\/\/|#).*/;
const DATA_URI = /^(data:)([^,]*),(.*)/i;
Expand Down Expand Up @@ -367,6 +381,7 @@ function serializeNode(
maskInputOptions: MaskInputOptions;
maskTextFn: MaskTextFn | undefined;
maskInputFn: MaskInputFn | undefined;
inlineImages: boolean;
recordCanvas: boolean;
keepIframeSrcFn: KeepIframeSrcFn;
},
Expand All @@ -381,6 +396,7 @@ function serializeNode(
maskInputOptions = {},
maskTextFn,
maskInputFn,
inlineImages,
recordCanvas,
keepIframeSrcFn,
} = options;
Expand Down Expand Up @@ -496,6 +512,19 @@ function serializeNode(
if (tagName === 'canvas' && recordCanvas) {
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
}
// save image offline
if (tagName === 'img' && inlineImages && canvasService && canvasCtx) {
const image = (n as HTMLImageElement);
image.crossOrigin = 'anonymous';
try {
canvasService.width = image.naturalWidth;
canvasService.height = image.naturalHeight;
canvasCtx.drawImage(image, 0, 0);
attributes.rr_dataURL = canvasService.toDataURL();
} catch {
// ignore error
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we log something here, e.g. with console.warn?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this happened on the record side, at least we need an option to do a log.

And this is out of the scope of this PR, feel free to open a new one! Thanks.

}
}
// media elements
if (tagName === 'audio' || tagName === 'video') {
attributes.rr_mediaState = (n as HTMLMediaElement).paused
Expand Down Expand Up @@ -709,6 +738,7 @@ export function serializeNodeWithId(
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
keepIframeSrcFn?: KeepIframeSrcFn;
inlineImages?: boolean;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
Expand All @@ -729,6 +759,7 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
inlineImages = false,
recordCanvas = false,
onSerialize,
onIframeLoad,
Expand All @@ -746,6 +777,7 @@ export function serializeNodeWithId(
maskInputOptions,
maskTextFn,
maskInputFn,
inlineImages,
recordCanvas,
keepIframeSrcFn,
});
Expand Down Expand Up @@ -798,6 +830,9 @@ export function serializeNodeWithId(
) {
preserveWhiteSpace = false;
}
if (inlineImages) {
initCanvasService(doc);
}
const bypassOptions = {
doc,
map,
Expand All @@ -811,6 +846,7 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
Expand Down Expand Up @@ -863,6 +899,7 @@ export function serializeNodeWithId(
maskTextFn,
maskInputFn,
slimDOMOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
Expand Down Expand Up @@ -895,6 +932,7 @@ function snapshot(
maskTextFn?: MaskTextFn;
maskInputFn?: MaskTextFn;
slimDOM?: boolean | SlimDOMOptions;
inlineImages?: boolean;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
Expand All @@ -909,6 +947,7 @@ function snapshot(
maskTextClass = 'rr-mask',
maskTextSelector = null,
inlineStylesheet = true,
inlineImages = false,
recordCanvas = false,
maskAllInputs = false,
maskTextFn,
Expand Down Expand Up @@ -978,6 +1017,7 @@ function snapshot(
maskTextFn,
maskInputFn,
slimDOMOptions,
inlineImages,
recordCanvas,
preserveWhiteSpace,
onSerialize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ exports[`integration tests [html file]: picture.html 1`] = `
<source type=\\"image/webp\\" srcset=\\"http://localhost:3030/assets/img/characters/robot.webp\\" />
<img src=\\"http://localhost:3030/assets/img/characters/robot.png\\" />
</picture>
<img src=\\"http://localhost:3030/images/robot.png\\" alt=\\"This is a robot\\" />
</body></html>"
`;

Expand Down
1 change: 1 addition & 0 deletions packages/rrweb-snapshot/test/html/picture.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
<source type="image/webp" srcset="assets/img/characters/robot.webp" />
<img src="assets/img/characters/robot.png" />
</picture>
<img src="/images/robot.png" alt="This is a robot" />
</body>
</html>
Binary file added packages/rrweb-snapshot/test/images/robot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions packages/rrweb-snapshot/test/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const startServer = () =>
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.png': 'image/png',
};
const s = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url!);
Expand Down Expand Up @@ -190,6 +191,25 @@ iframe.contentDocument.querySelector('center').clientHeight
'rebuilt height (${rebuildRenderedHeight}) should equal original height (${renderedHeight})',
);
});

it('correctly saves images offline', async () => {
const page: puppeteer.Page = await browser.newPage();
// console for debug
// tslint:disable-next-line: no-console
page.on('console', (msg) => console.log(msg.text()));

await page.goto('http://localhost:3030/html/picture.html', { waitUntil: 'load' });
await page.waitForSelector('img', { timeout: 1000 });

const snapshot = await page.evaluate(`${code}
const [snap] = rrweb.snapshot(document, {inlineImages: true, inlineStylesheet: false});
JSON.stringify(snap, null, 2);
`);

assert(snapshot.includes('"rr_dataURL"'));
assert(snapshot.includes('data:image/png;base64,'));
});

});

describe('iframe integration tests', function (this: ISuite) {
Expand Down
2 changes: 2 additions & 0 deletions packages/rrweb-snapshot/typings/snapshot.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export declare function serializeNodeWithId(n: Node | INode, options: {
maskInputFn: MaskInputFn | undefined;
slimDOMOptions: SlimDOMOptions;
keepIframeSrcFn?: KeepIframeSrcFn;
inlineImages?: boolean;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
Expand All @@ -35,6 +36,7 @@ declare function snapshot(n: Document, options?: {
maskTextFn?: MaskTextFn;
maskInputFn?: MaskTextFn;
slimDOM?: boolean | SlimDOMOptions;
inlineImages?: boolean;
recordCanvas?: boolean;
preserveWhiteSpace?: boolean;
onSerialize?: (n: INode) => unknown;
Expand Down