Skip to content

Commit 5dff0c0

Browse files
committed
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 - if enabled, save PDF with background using screen media mode
1 parent 57fb0da commit 5dff0c0

File tree

2 files changed

+150
-18
lines changed

2 files changed

+150
-18
lines changed

src/recorder.ts

+104
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

+46-18
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,10 @@ class ArchiveWebApp extends ReplayWebApp {
124126
this.archiveStorage = (await getLocalOption("archiveStorage")) === "1";
125127

126128
this.archiveFlash = (await getLocalOption("archiveFlash")) === "1";
129+
130+
this.archiveScreenshots = (await getLocalOption("archiveScreenshots")) === "1";
131+
132+
this.archivePDF = (await getLocalOption("archivePDF")) === "1";
127133
}
128134

129135
async doBtrixLogin() {
@@ -1029,9 +1035,32 @@ class ArchiveWebApp extends ReplayWebApp {
10291035
>
10301036
${this.settingsTab === "prefs"
10311037
? html`<fieldset>
1032-
<div class="is-size-6">
1033-
Control archiving of optional extensions and sensitive browser
1034-
data.
1038+
<div class="is-size-6 mt-4">
1039+
Optional archiving features:
1040+
</div>
1041+
<div class="field is-size-6 mt-4">
1042+
<input
1043+
name="prefArchiveScreenshots"
1044+
id="archiveScreenshots"
1045+
class="checkbox"
1046+
type="checkbox"
1047+
?checked="${this.archiveScreenshots}"
1048+
/><span class="ml-1">Save Screenshots</span>
1049+
<p class="is-size-7 mt-1">
1050+
Save screenshot + thumbnail of every page on load. Screenshot will be saved as soon as page is done loading.
1051+
</p>
1052+
</div>
1053+
<div class="field is-size-6 mt-4">
1054+
<input
1055+
name="prefArchivePDF"
1056+
id="archivePDF"
1057+
class="checkbox"
1058+
type="checkbox"
1059+
?checked="${this.archivePDF}"
1060+
/><span class="ml-1">Save PDFs</span>
1061+
<p class="is-size-7 mt-1">
1062+
Save PDF of each page after page loads (experimental).
1063+
</p>
10351064
</div>
10361065
<div class="field is-size-6 mt-4">
10371066
<input
@@ -1047,6 +1076,10 @@ class ArchiveWebApp extends ReplayWebApp {
10471076
enable only when archiving websites that contain Flash.
10481077
</p>
10491078
</div>
1079+
<hr/>
1080+
<div class="is-size-6">
1081+
Privacy related settings:
1082+
</div>
10501083
<div class="field is-size-6 mt-4">
10511084
<input
10521085
name="prefArchiveCookies"
@@ -1441,23 +1474,18 @@ class ArchiveWebApp extends ReplayWebApp {
14411474
}
14421475
}
14431476

1444-
const archiveCookies = this.renderRoot.querySelector("#archiveCookies");
1445-
const archiveStorage = this.renderRoot.querySelector("#archiveStorage");
1446-
const archiveFlash = this.renderRoot.querySelector("#archiveFlash");
1477+
const options = ["Cookies", "Storage", "Flash", "Screenshots", "PDF"];
14471478

1448-
if (archiveCookies) {
1449-
this.archiveCookies = (archiveCookies as HTMLInputElement).checked;
1450-
await setLocalOption("archiveCookies", this.archiveCookies ? "1" : "0");
1451-
}
1479+
for (const option of options) {
1480+
const name = "archive" + option;
1481+
const elem = this.renderRoot.querySelector("#" + name);
14521482

1453-
if (archiveStorage) {
1454-
this.archiveStorage = (archiveStorage as HTMLInputElement).checked;
1455-
await setLocalOption("archiveStorage", this.archiveStorage ? "1" : "0");
1456-
}
1457-
1458-
if (archiveFlash) {
1459-
this.archiveFlash = (archiveFlash as HTMLInputElement).checked;
1460-
await setLocalOption("archiveFlash", this.archiveFlash ? "1" : "0");
1483+
if (elem) {
1484+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1485+
(this as any)[name] = (elem as HTMLInputElement).checked;
1486+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1487+
await setLocalOption(name, (this as any)[name] ? "1" : "0");
1488+
}
14611489
}
14621490

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

0 commit comments

Comments
 (0)