Skip to content

Commit 1724dd0

Browse files
authored
Support for generating screenshots and (experimental) PDF per page (#297)
options to enable archiving screenshots + PDF: - add screenshot and PDF archiving checkboxes to settings - if enabled, save 'urn:view' and 'urn:thumbnail', scaled via offscreencanvas with same dimensions as Browsertrix (enabled by default) - if enabled, save PDF with background using screen media mode (experimental), saved with 'urn:pdf' prefix
1 parent 2892141 commit 1724dd0

File tree

2 files changed

+158
-18
lines changed

2 files changed

+158
-18
lines changed

src/recorder.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ class Recorder {
5858
archiveStorage = false;
5959
archiveCookies = false;
6060
archiveFlash = false;
61+
archiveScreenshots = false;
62+
archivePDF = false;
6163

6264
_fetchQueue: FetchEntry[] = [];
6365

@@ -158,6 +160,8 @@ class Recorder {
158160
this.archiveCookies = (await getLocalOption("archiveCookies")) === "1";
159161
this.archiveStorage = (await getLocalOption("archiveStorage")) === "1";
160162
this.archiveFlash = (await getLocalOption("archiveFlash")) === "1";
163+
this.archiveScreenshots = (await getLocalOption("archiveScreenshots")) === "1";
164+
this.archivePDF = (await getLocalOption("archivePDF")) === "1";
161165
}
162166

163167
// @ts-expect-error - TS7006 - Parameter 'autorun' implicitly has an 'any' type.
@@ -930,6 +934,98 @@ class Recorder {
930934
return success;
931935
}
932936

937+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
938+
async savePDF(pageInfo: any) {
939+
// @ts-expect-error: ignore param
940+
await this.send("Emulation.setEmulatedMedia", {type: "screen"});
941+
942+
// @ts-expect-error: ignore param
943+
const resp = await this.send("Page.printToPDF", {printBackground: true});
944+
945+
// @ts-expect-error: ignore param
946+
await this.send("Emulation.setEmulatedMedia", {type: ""});
947+
948+
const payload = Buffer.from(resp.data, "base64");
949+
const mime = "application/pdf";
950+
951+
const fullData = {
952+
url: "urn:pdf:" + pageInfo.url,
953+
ts: new Date().getTime(),
954+
status: 200,
955+
statusText: "OK",
956+
pageId: pageInfo.id,
957+
mime,
958+
respHeaders: {"Content-Type": mime, "Content-Length": payload.length + ""},
959+
reqHeaders: {},
960+
payload,
961+
extraOpts: {resource: true},
962+
};
963+
964+
console.log("pdf", payload.length);
965+
966+
// @ts-expect-error - TS2339 - Property '_doAddResource' does not exist on type 'Recorder'.
967+
await this._doAddResource(fullData);
968+
}
969+
970+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
971+
async saveScreenshot(pageInfo: any) {
972+
973+
// View Screenshot
974+
const width = 1920;
975+
const height = 1080;
976+
977+
// @ts-expect-error: ignore param
978+
await this.send("Emulation.setDeviceMetricsOverride", {width, height, deviceScaleFactor: 0, mobile: false});
979+
// @ts-expect-error: ignore param
980+
const resp = await this.send("Page.captureScreenshot", {format: "png"});
981+
982+
const payload = Buffer.from(resp.data, "base64");
983+
const blob = new Blob([payload], {type: "image/png"});
984+
985+
await this.send("Emulation.clearDeviceMetricsOverride");
986+
987+
const mime = "image/png";
988+
989+
const fullData = {
990+
url: "urn:view:" + pageInfo.url,
991+
ts: new Date().getTime(),
992+
status: 200,
993+
statusText: "OK",
994+
pageId: pageInfo.id,
995+
mime,
996+
respHeaders: {"Content-Type": mime, "Content-Length": payload.length + ""},
997+
reqHeaders: {},
998+
payload,
999+
extraOpts: {resource: true},
1000+
};
1001+
1002+
const thumbWidth = 640;
1003+
const thumbHeight = 360;
1004+
1005+
const bitmap = await self.createImageBitmap(blob, {resizeWidth: thumbWidth, resizeHeight: thumbHeight});
1006+
1007+
const canvas = new OffscreenCanvas(thumbWidth, thumbWidth);
1008+
const context = canvas.getContext("bitmaprenderer")!;
1009+
context.transferFromImageBitmap(bitmap);
1010+
1011+
const resizedBlob = await canvas.convertToBlob({type: "image/png"});
1012+
1013+
const thumbPayload = new Uint8Array(await resizedBlob.arrayBuffer());
1014+
1015+
const thumbData = {...fullData,
1016+
url: "urn:thumbnail:" + pageInfo.url,
1017+
respHeaders: {"Content-Type": mime, "Content-Length": thumbPayload.length + ""},
1018+
payload: thumbPayload
1019+
};
1020+
1021+
// @ts-expect-error - TS2339 - Property '_doAddResource' does not exist on type 'Recorder'.
1022+
await this._doAddResource(fullData);
1023+
1024+
1025+
// @ts-expect-error - TS2339 - Property '_doAddResource' does not exist on type 'Recorder'.
1026+
await this._doAddResource(thumbData);
1027+
}
1028+
9331029
async getFullText(finishing = false) {
9341030
// @ts-expect-error - TS2339 - Property 'pageInfo' does not exist on type 'Recorder'. | TS2339 - Property 'pageInfo' does not exist on type 'Recorder'.
9351031
if (!this.pageInfo?.url) {
@@ -1189,6 +1285,14 @@ class Recorder {
11891285
// @ts-expect-error - TS2339 - Property 'pageInfo' does not exist on type 'Recorder'.
11901286
const pageInfo = this.pageInfo;
11911287

1288+
if (this.archiveScreenshots) {
1289+
await this.saveScreenshot(pageInfo);
1290+
}
1291+
1292+
if (this.archivePDF) {
1293+
await this.savePDF(pageInfo);
1294+
}
1295+
11921296
const [domSnapshot, favIcon] = await Promise.all([
11931297
this.getFullText(),
11941298
// @ts-expect-error - TS2339 - Property 'getFavIcon' does not exist on type 'Recorder'.

src/ui/app.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ class ArchiveWebApp extends ReplayWebApp {
5959
archiveCookies: boolean | null = null;
6060
archiveStorage: boolean | null = null;
6161
archiveFlash: boolean | null = null;
62+
archiveScreenshots: boolean | null = null;
63+
archivePDF: boolean | null = null;
6264

6365
showIpfsShareFailed = false;
6466

@@ -124,6 +126,18 @@ class ArchiveWebApp extends ReplayWebApp {
124126
this.archiveStorage = (await getLocalOption("archiveStorage")) === "1";
125127

126128
this.archiveFlash = (await getLocalOption("archiveFlash")) === "1";
129+
130+
const archiveScreenshots = await getLocalOption("archiveScreenshots");
131+
132+
// default to true if unset to enable screenshots!
133+
if (archiveScreenshots === null || archiveScreenshots === undefined) {
134+
await setLocalOption("archiveScreenshots", "1");
135+
this.archiveScreenshots = true;
136+
} else {
137+
this.archiveScreenshots = archiveScreenshots === "1";
138+
}
139+
140+
this.archivePDF = (await getLocalOption("archivePDF")) === "1";
127141
}
128142

129143
async doBtrixLogin() {
@@ -1029,9 +1043,32 @@ class ArchiveWebApp extends ReplayWebApp {
10291043
>
10301044
${this.settingsTab === "prefs"
10311045
? html`<fieldset>
1032-
<div class="is-size-6">
1033-
Control archiving of optional extensions and sensitive browser
1034-
data.
1046+
<div class="is-size-6 mt-4">
1047+
Optional archiving features:
1048+
</div>
1049+
<div class="field is-size-6 mt-4">
1050+
<input
1051+
name="prefArchiveScreenshots"
1052+
id="archiveScreenshots"
1053+
class="checkbox"
1054+
type="checkbox"
1055+
?checked="${this.archiveScreenshots}"
1056+
/><span class="ml-1">Save Screenshots</span>
1057+
<p class="is-size-7 mt-1">
1058+
Save screenshot + thumbnail of every page on load. Screenshot will be saved as soon as page is done loading.
1059+
</p>
1060+
</div>
1061+
<div class="field is-size-6 mt-4">
1062+
<input
1063+
name="prefArchivePDF"
1064+
id="archivePDF"
1065+
class="checkbox"
1066+
type="checkbox"
1067+
?checked="${this.archivePDF}"
1068+
/><span class="ml-1">Save PDFs</span>
1069+
<p class="is-size-7 mt-1">
1070+
Save PDF of each page after page loads (experimental).
1071+
</p>
10351072
</div>
10361073
<div class="field is-size-6 mt-4">
10371074
<input
@@ -1047,6 +1084,10 @@ class ArchiveWebApp extends ReplayWebApp {
10471084
enable only when archiving websites that contain Flash.
10481085
</p>
10491086
</div>
1087+
<hr/>
1088+
<div class="is-size-6">
1089+
Privacy related settings:
1090+
</div>
10501091
<div class="field is-size-6 mt-4">
10511092
<input
10521093
name="prefArchiveCookies"
@@ -1441,23 +1482,18 @@ class ArchiveWebApp extends ReplayWebApp {
14411482
}
14421483
}
14431484

1444-
const archiveCookies = this.renderRoot.querySelector("#archiveCookies");
1445-
const archiveStorage = this.renderRoot.querySelector("#archiveStorage");
1446-
const archiveFlash = this.renderRoot.querySelector("#archiveFlash");
1447-
1448-
if (archiveCookies) {
1449-
this.archiveCookies = (archiveCookies as HTMLInputElement).checked;
1450-
await setLocalOption("archiveCookies", this.archiveCookies ? "1" : "0");
1451-
}
1485+
const options = ["Cookies", "Storage", "Flash", "Screenshots", "PDF"];
14521486

1453-
if (archiveStorage) {
1454-
this.archiveStorage = (archiveStorage as HTMLInputElement).checked;
1455-
await setLocalOption("archiveStorage", this.archiveStorage ? "1" : "0");
1456-
}
1487+
for (const option of options) {
1488+
const name = "archive" + option;
1489+
const elem = this.renderRoot.querySelector("#" + name);
14571490

1458-
if (archiveFlash) {
1459-
this.archiveFlash = (archiveFlash as HTMLInputElement).checked;
1460-
await setLocalOption("archiveFlash", this.archiveFlash ? "1" : "0");
1491+
if (elem) {
1492+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1493+
(this as any)[name] = (elem as HTMLInputElement).checked;
1494+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1495+
await setLocalOption(name, (this as any)[name] ? "1" : "0");
1496+
}
14611497
}
14621498

14631499
localStorage.setItem("settingsTab", this.settingsTab);

0 commit comments

Comments
 (0)