@btraut/browser-bridge 0.4.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -142,6 +142,59 @@ var wrapChromeVoid = (invoker) => {
142
142
  });
143
143
  });
144
144
  };
145
+ var delayMs = async (ms) => {
146
+ if (!Number.isFinite(ms) || ms <= 0) {
147
+ return;
148
+ }
149
+ await new Promise((resolve) => {
150
+ self.setTimeout(resolve, ms);
151
+ });
152
+ };
153
+ var parseDataUrl = (dataUrl) => {
154
+ const match = /^data:([^;]+);base64,(.*)$/s.exec(dataUrl);
155
+ if (!match) {
156
+ return null;
157
+ }
158
+ return { mime: match[1] ?? "application/octet-stream", base64: match[2] };
159
+ };
160
+ var arrayBufferToBase64 = (buffer) => {
161
+ const bytes = new Uint8Array(buffer);
162
+ const chunkSize = 32768;
163
+ let binary = "";
164
+ for (let i = 0; i < bytes.length; i += chunkSize) {
165
+ const chunk = bytes.subarray(i, i + chunkSize);
166
+ binary += String.fromCharCode(...chunk);
167
+ }
168
+ return btoa(binary);
169
+ };
170
+ var renderDataUrlToFormat = async (dataUrl, format, quality) => {
171
+ const parsed = parseDataUrl(dataUrl);
172
+ if (!parsed) {
173
+ throw new Error("Invalid screenshot data URL.");
174
+ }
175
+ const blob = await (await fetch(dataUrl)).blob();
176
+ const bitmap = await createImageBitmap(blob);
177
+ try {
178
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
179
+ const ctx = canvas.getContext("2d");
180
+ if (!ctx) {
181
+ throw new Error("Canvas context unavailable.");
182
+ }
183
+ ctx.drawImage(bitmap, 0, 0);
184
+ const mime = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
185
+ const q = typeof quality === "number" && Number.isFinite(quality) ? Math.max(0, Math.min(1, quality / 100)) : void 0;
186
+ const out = format === "png" ? await canvas.convertToBlob({ type: mime }) : await canvas.convertToBlob({ type: mime, quality: q });
187
+ const base64 = arrayBufferToBase64(await out.arrayBuffer());
188
+ return {
189
+ mime,
190
+ data_base64: base64,
191
+ width_px: bitmap.width,
192
+ height_px: bitmap.height
193
+ };
194
+ } finally {
195
+ bitmap.close();
196
+ }
197
+ };
145
198
  var readCorePort = async () => {
146
199
  return await new Promise((resolve) => {
147
200
  chrome.storage.local.get(
@@ -797,6 +850,366 @@ var DriveSocket = class {
797
850
  }
798
851
  return;
799
852
  }
853
+ case "drive.screenshot": {
854
+ const params = message.params ?? {};
855
+ let tabId = params.tab_id;
856
+ if (tabId !== void 0 && typeof tabId !== "number") {
857
+ respondError({
858
+ code: "INVALID_ARGUMENT",
859
+ message: "tab_id must be a number when provided.",
860
+ retryable: false
861
+ });
862
+ return;
863
+ }
864
+ if (tabId === void 0) {
865
+ tabId = await getActiveTabId();
866
+ }
867
+ const mode = params.mode === "full_page" || params.mode === "viewport" || params.mode === "element" ? params.mode : "viewport";
868
+ const format = params.format === "jpeg" || params.format === "webp" ? params.format : "png";
869
+ const quality = typeof params.quality === "number" && Number.isFinite(params.quality) ? Math.max(0, Math.min(100, Math.floor(params.quality))) : void 0;
870
+ const tab = await getTab(tabId);
871
+ const url = tab.url;
872
+ if (typeof url === "string" && isRestrictedUrl(url)) {
873
+ respondError({
874
+ code: "NOT_SUPPORTED",
875
+ message: "Screenshots are not supported for this URL.",
876
+ retryable: false,
877
+ details: { url }
878
+ });
879
+ return;
880
+ }
881
+ const windowId = tab.windowId;
882
+ if (typeof windowId !== "number") {
883
+ respondError({
884
+ code: "TAB_NOT_FOUND",
885
+ message: "window_id missing for tab.",
886
+ retryable: false
887
+ });
888
+ return;
889
+ }
890
+ if (typeof OffscreenCanvas === "undefined") {
891
+ respondError({
892
+ code: "NOT_SUPPORTED",
893
+ message: "OffscreenCanvas is unavailable in this extension host.",
894
+ retryable: false
895
+ });
896
+ return;
897
+ }
898
+ const captureVisible = async () => {
899
+ return await wrapChromeCallback(
900
+ (callback) => chrome.tabs.captureVisibleTab(
901
+ windowId,
902
+ { format: "png" },
903
+ callback
904
+ )
905
+ );
906
+ };
907
+ const scrollTo = async (top, left) => {
908
+ const result = await sendToTab(tabId, "drive.scroll", {
909
+ top,
910
+ left,
911
+ behavior: "auto",
912
+ tab_id: tabId
913
+ });
914
+ if (!result.ok) {
915
+ throw new Error(result.error.message);
916
+ }
917
+ };
918
+ const getMetaInfo = async () => {
919
+ const meta = await sendToTab(
920
+ tabId,
921
+ "drive.screenshot_meta"
922
+ );
923
+ if (!meta.ok) {
924
+ throw new Error(meta.error.message);
925
+ }
926
+ const payload = meta.result;
927
+ if (!payload || typeof payload !== "object") {
928
+ throw new Error("Invalid screenshot metadata response.");
929
+ }
930
+ const record = payload;
931
+ const viewportHeight = record.viewportHeight;
932
+ const scrollHeight = record.scrollHeight;
933
+ const scrollY = record.scrollY;
934
+ const scrollX = record.scrollX;
935
+ const dpr = record.devicePixelRatio;
936
+ if (typeof viewportHeight !== "number" || !Number.isFinite(viewportHeight) || viewportHeight <= 0) {
937
+ throw new Error(
938
+ "viewportHeight missing from screenshot metadata."
939
+ );
940
+ }
941
+ if (typeof scrollHeight !== "number" || !Number.isFinite(scrollHeight) || scrollHeight <= 0) {
942
+ throw new Error("scrollHeight missing from screenshot metadata.");
943
+ }
944
+ const devicePixelRatio = typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : 1;
945
+ const fullHeightPx = Math.round(scrollHeight * devicePixelRatio);
946
+ const maxHeightPx = 5e4;
947
+ if (fullHeightPx > maxHeightPx) {
948
+ throw new Error(
949
+ `Page is too tall to capture (max ${maxHeightPx}px).`
950
+ );
951
+ }
952
+ const maxScrollY = Math.max(0, scrollHeight - viewportHeight);
953
+ const step = viewportHeight;
954
+ const positions = [];
955
+ for (let y = 0; y < maxScrollY; y += step) {
956
+ positions.push(y);
957
+ }
958
+ positions.push(maxScrollY);
959
+ const maxTiles = 200;
960
+ if (positions.length > maxTiles) {
961
+ throw new Error(
962
+ `Page requires too many tiles to capture (${positions.length}).`
963
+ );
964
+ }
965
+ return {
966
+ viewportHeight,
967
+ scrollHeight,
968
+ scrollY: typeof scrollY === "number" && Number.isFinite(scrollY) ? scrollY : 0,
969
+ scrollX: typeof scrollX === "number" && Number.isFinite(scrollX) ? scrollX : 0,
970
+ devicePixelRatio,
971
+ fullHeightPx,
972
+ positions
973
+ };
974
+ };
975
+ const canvasToResult = async (canvas) => {
976
+ const mime = format === "jpeg" ? "image/jpeg" : format === "webp" ? "image/webp" : "image/png";
977
+ const q = typeof quality === "number" && Number.isFinite(quality) ? Math.max(0, Math.min(1, quality / 100)) : void 0;
978
+ const blob = format === "png" ? await canvas.convertToBlob({ type: mime }) : await canvas.convertToBlob({ type: mime, quality: q });
979
+ const base64 = arrayBufferToBase64(await blob.arrayBuffer());
980
+ return {
981
+ mime,
982
+ data_base64: base64,
983
+ width_px: canvas.width,
984
+ height_px: canvas.height
985
+ };
986
+ };
987
+ const captureFullPageCanvas = async (metaInfo) => {
988
+ try {
989
+ await scrollTo(0, 0);
990
+ await delayMs(100);
991
+ const firstDataUrl = await captureVisible();
992
+ const firstBlob = await (await fetch(firstDataUrl)).blob();
993
+ const firstBitmap = await createImageBitmap(firstBlob);
994
+ const canvas = new OffscreenCanvas(
995
+ firstBitmap.width,
996
+ metaInfo.fullHeightPx
997
+ );
998
+ const ctx = canvas.getContext("2d");
999
+ if (!ctx) {
1000
+ firstBitmap.close();
1001
+ throw new Error("Canvas context unavailable.");
1002
+ }
1003
+ const drawTile = (bitmap, yCss) => {
1004
+ const destY = Math.round(yCss * metaInfo.devicePixelRatio);
1005
+ const remaining = metaInfo.fullHeightPx - destY;
1006
+ if (remaining <= 0) {
1007
+ return;
1008
+ }
1009
+ const drawHeight = Math.min(bitmap.height, remaining);
1010
+ ctx.drawImage(
1011
+ bitmap,
1012
+ 0,
1013
+ 0,
1014
+ bitmap.width,
1015
+ drawHeight,
1016
+ 0,
1017
+ destY,
1018
+ bitmap.width,
1019
+ drawHeight
1020
+ );
1021
+ };
1022
+ drawTile(firstBitmap, 0);
1023
+ firstBitmap.close();
1024
+ for (const y of metaInfo.positions.slice(1)) {
1025
+ await scrollTo(y, 0);
1026
+ await delayMs(100);
1027
+ const dataUrl = await captureVisible();
1028
+ const blob = await (await fetch(dataUrl)).blob();
1029
+ const bitmap = await createImageBitmap(blob);
1030
+ drawTile(bitmap, y);
1031
+ bitmap.close();
1032
+ }
1033
+ return canvas;
1034
+ } finally {
1035
+ try {
1036
+ await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
1037
+ } catch {
1038
+ }
1039
+ }
1040
+ };
1041
+ if (mode === "viewport") {
1042
+ try {
1043
+ const dataUrl = await captureVisible();
1044
+ const rendered = await renderDataUrlToFormat(
1045
+ dataUrl,
1046
+ format,
1047
+ quality
1048
+ );
1049
+ respondOk(rendered);
1050
+ } catch (error) {
1051
+ respondError({
1052
+ code: "ARTIFACT_IO_ERROR",
1053
+ message: error instanceof Error ? error.message : "Failed to capture screenshot.",
1054
+ retryable: false
1055
+ });
1056
+ }
1057
+ return;
1058
+ }
1059
+ if (mode === "element") {
1060
+ const selector = params.selector;
1061
+ if (typeof selector !== "string" || selector.trim().length === 0) {
1062
+ respondError({
1063
+ code: "INVALID_ARGUMENT",
1064
+ message: "selector must be a non-empty string.",
1065
+ retryable: false
1066
+ });
1067
+ return;
1068
+ }
1069
+ let metaInfo;
1070
+ try {
1071
+ metaInfo = await getMetaInfo();
1072
+ } catch (error) {
1073
+ respondError({
1074
+ code: "EVALUATION_FAILED",
1075
+ message: error instanceof Error ? error.message : "Failed to read screenshot metadata.",
1076
+ retryable: false
1077
+ });
1078
+ return;
1079
+ }
1080
+ const element = await sendToTab(
1081
+ tabId,
1082
+ "drive.screenshot_element",
1083
+ { selector }
1084
+ );
1085
+ if (!element.ok) {
1086
+ respondError(element.error);
1087
+ return;
1088
+ }
1089
+ const payload = element.result;
1090
+ if (!payload || typeof payload !== "object") {
1091
+ respondError({
1092
+ code: "EVALUATION_FAILED",
1093
+ message: "Invalid element metadata response.",
1094
+ retryable: false
1095
+ });
1096
+ return;
1097
+ }
1098
+ const record = payload;
1099
+ const viewportLeft = record.viewportLeft;
1100
+ const viewportTop = record.viewportTop;
1101
+ const viewportWidth = record.viewportWidth;
1102
+ const viewportHeight = record.viewportHeight;
1103
+ const width = record.width;
1104
+ const height = record.height;
1105
+ const pageX = record.pageX;
1106
+ const pageY = record.pageY;
1107
+ const dpr = record.devicePixelRatio;
1108
+ const devicePixelRatio = typeof dpr === "number" && Number.isFinite(dpr) && dpr > 0 ? dpr : metaInfo.devicePixelRatio;
1109
+ if (typeof viewportLeft !== "number" || typeof viewportTop !== "number" || typeof viewportWidth !== "number" || typeof viewportHeight !== "number" || typeof width !== "number" || typeof height !== "number" || typeof pageX !== "number" || typeof pageY !== "number" || !Number.isFinite(viewportLeft) || !Number.isFinite(viewportTop) || !Number.isFinite(viewportWidth) || !Number.isFinite(viewportHeight) || !Number.isFinite(width) || !Number.isFinite(height) || !Number.isFinite(pageX) || !Number.isFinite(pageY)) {
1110
+ respondError({
1111
+ code: "EVALUATION_FAILED",
1112
+ message: "Invalid element bounding box metadata.",
1113
+ retryable: false
1114
+ });
1115
+ return;
1116
+ }
1117
+ const fitsInViewport = viewportLeft >= 0 && viewportTop >= 0 && viewportLeft + width <= viewportWidth && viewportTop + height <= viewportHeight;
1118
+ const cropW = Math.max(1, Math.round(width * devicePixelRatio));
1119
+ const cropH = Math.max(1, Math.round(height * devicePixelRatio));
1120
+ try {
1121
+ if (fitsInViewport) {
1122
+ const dataUrl = await captureVisible();
1123
+ const blob = await (await fetch(dataUrl)).blob();
1124
+ const bitmap = await createImageBitmap(blob);
1125
+ try {
1126
+ const cropX2 = Math.max(
1127
+ 0,
1128
+ Math.round(viewportLeft * devicePixelRatio)
1129
+ );
1130
+ const cropY2 = Math.max(
1131
+ 0,
1132
+ Math.round(viewportTop * devicePixelRatio)
1133
+ );
1134
+ const srcW2 = Math.min(cropW, bitmap.width - cropX2);
1135
+ const srcH2 = Math.min(cropH, bitmap.height - cropY2);
1136
+ if (srcW2 <= 0 || srcH2 <= 0) {
1137
+ throw new Error("Element is outside screenshot bounds.");
1138
+ }
1139
+ const cropCanvas2 = new OffscreenCanvas(srcW2, srcH2);
1140
+ const ctx2 = cropCanvas2.getContext("2d");
1141
+ if (!ctx2) {
1142
+ throw new Error("Canvas context unavailable.");
1143
+ }
1144
+ ctx2.drawImage(
1145
+ bitmap,
1146
+ cropX2,
1147
+ cropY2,
1148
+ srcW2,
1149
+ srcH2,
1150
+ 0,
1151
+ 0,
1152
+ srcW2,
1153
+ srcH2
1154
+ );
1155
+ respondOk(await canvasToResult(cropCanvas2));
1156
+ } finally {
1157
+ bitmap.close();
1158
+ }
1159
+ return;
1160
+ }
1161
+ const fullCanvas = await captureFullPageCanvas(metaInfo);
1162
+ const cropX = Math.max(0, Math.round(pageX * devicePixelRatio));
1163
+ const cropY = Math.max(0, Math.round(pageY * devicePixelRatio));
1164
+ const srcW = Math.min(cropW, fullCanvas.width - cropX);
1165
+ const srcH = Math.min(cropH, fullCanvas.height - cropY);
1166
+ if (srcW <= 0 || srcH <= 0) {
1167
+ throw new Error("Element is outside screenshot bounds.");
1168
+ }
1169
+ const cropCanvas = new OffscreenCanvas(srcW, srcH);
1170
+ const ctx = cropCanvas.getContext("2d");
1171
+ if (!ctx) {
1172
+ throw new Error("Canvas context unavailable.");
1173
+ }
1174
+ ctx.drawImage(
1175
+ fullCanvas,
1176
+ cropX,
1177
+ cropY,
1178
+ srcW,
1179
+ srcH,
1180
+ 0,
1181
+ 0,
1182
+ srcW,
1183
+ srcH
1184
+ );
1185
+ respondOk(await canvasToResult(cropCanvas));
1186
+ } catch (error) {
1187
+ respondError({
1188
+ code: "ARTIFACT_IO_ERROR",
1189
+ message: error instanceof Error ? error.message : "Failed to capture element screenshot.",
1190
+ retryable: false
1191
+ });
1192
+ } finally {
1193
+ try {
1194
+ await scrollTo(metaInfo.scrollY, metaInfo.scrollX);
1195
+ } catch {
1196
+ }
1197
+ }
1198
+ return;
1199
+ }
1200
+ try {
1201
+ const metaInfo = await getMetaInfo();
1202
+ const canvas = await captureFullPageCanvas(metaInfo);
1203
+ respondOk(await canvasToResult(canvas));
1204
+ } catch (error) {
1205
+ respondError({
1206
+ code: "ARTIFACT_IO_ERROR",
1207
+ message: error instanceof Error ? error.message : "Failed to capture full page screenshot.",
1208
+ retryable: false
1209
+ });
1210
+ }
1211
+ return;
1212
+ }
800
1213
  default:
801
1214
  respondError({
802
1215
  code: "NOT_IMPLEMENTED",