返回 DeepSeek-Reasonix
screenshot.ts
根目录 / desktop / electron / src / main / browser / screenshot.ts
1 import { existsSync, statSync, writeFileSync } from "node:fs";
2 import { isAbsolute, join } from "node:path";
3 import type { DocumentBinding } from "./documents.js";
4 import type { GuestPage } from "./guestView.js";
5 import { resolveRef } from "./refResolver.js";
6
7 export interface ScreenshotRequest {
8 ref: string;
9 fullPage: boolean;
10 directory: string;
11 }
12
13 export interface ScreenshotResult {
14 path: string;
15 mime: "image/png";
16 width: number;
17 height: number;
18 reason?: string;
19 }
20
21 export interface ScreenshotDeps {
22 directoryExists?(path: string): boolean;
23 writeFile?(path: string, data: Buffer): void;
24 now?(): number;
25 }
26
27 let sequence = 0;
28
29 const defaultDirectoryExists = (path: string) => {
30 try {
31 return existsSync(path) && statSync(path).isDirectory();
32 } catch {
33 return false;
34 }
35 };
36
37 export function pngSize(data: Buffer): { width: number; height: number } {
38 if (data.length < 24 || data.toString("latin1", 12, 16) !== "IHDR") return { width: 0, height: 0 };
39 return { width: data.readUInt32BE(16), height: data.readUInt32BE(20) };
40 }
41
42 export function screenshotPath(directory: string, now: number): string {
43 sequence += 1;
44 return join(directory, `shot-${now}-${sequence}.png`);
45 }
46
47 // Full-page captures go through the DevTools protocol because a
48 // WebContentsView cannot be resized past the window; everything else uses
49 // capturePage on the visible viewport or the element's rect.
50 export async function captureScreenshot(page: GuestPage, binding: DocumentBinding | null, zoom: number, request: ScreenshotRequest, deps: ScreenshotDeps = {}): Promise<ScreenshotResult> {
51 const directoryExists = deps.directoryExists ?? defaultDirectoryExists;
52 if (!isAbsolute(request.directory) || !directoryExists(request.directory)) throw new Error("screenshot directory must be an existing absolute path");
53 const target = screenshotPath(request.directory, (deps.now ?? Date.now)());
54 const write = deps.writeFile ?? ((path, data) => writeFileSync(path, data));
55 let reason: string | undefined;
56 let png: Buffer;
57 let size: { width: number; height: number };
58 if (request.fullPage) {
59 png = await captureFullPage(page);
60 size = pngSize(png);
61 } else {
62 let rect: Electron.Rectangle | undefined;
63 if (request.ref !== "") {
64 if (!binding) throw new Error("element screenshots need a snapshot first");
65 const resolved = await resolveRef(page, binding, request.ref, true);
66 if (resolved.ok) {
67 const { element } = resolved.value;
68 rect = {
69 x: Math.max(0, Math.floor(element.x * zoom)),
70 y: Math.max(0, Math.floor(element.y * zoom)),
71 width: Math.max(1, Math.ceil(element.width * zoom)),
72 height: Math.max(1, Math.ceil(element.height * zoom)),
73 };
74 } else reason = `captured the viewport instead: ${resolved.reason}`;
75 }
76 const image = await page.capturePage(rect);
77 png = image.toPNG();
78 size = image.getSize();
79 }
80 write(target, png);
81 return reason ? { path: target, mime: "image/png", width: size.width, height: size.height, reason } : { path: target, mime: "image/png", width: size.width, height: size.height };
82 }
83
84 async function captureFullPage(page: GuestPage): Promise<Buffer> {
85 const dbg = page.debugger;
86 const attached = dbg.isAttached();
87 if (!attached) dbg.attach("1.3");
88 try {
89 const result = (await dbg.sendCommand("Page.captureScreenshot", { format: "png", captureBeyondViewport: true, fromSurface: true })) as { data?: unknown };
90 if (typeof result?.data !== "string") throw new Error("Page.captureScreenshot returned no image");
91 return Buffer.from(result.data, "base64");
92 } finally {
93 if (!attached && dbg.isAttached()) dbg.detach();
94 }
95 }
96
96 lines TYPESCRIPT