增强截图功能:新增DOM元素选择器支持,优化截图参数配置

This commit is contained in:
fofolee 2025-01-23 10:03:50 +08:00
parent 4805cf6716
commit f4245a5744
2 changed files with 80 additions and 43 deletions

View File

@ -202,8 +202,8 @@ const initCDP = async (targetId) => {
if (!clients.has(targetId)) { if (!clients.has(targetId)) {
try { try {
const client = await CDP({ target: targetId }); const client = await CDP({ target: targetId });
const { Page, Runtime, Target, Network, Emulation } = client; const { Page, Runtime, Target, Network, Emulation, DOM } = client;
await Promise.all([Page.enable(), Runtime.enable()]); await Promise.all([Page.enable(), Runtime.enable(), DOM.enable()]);
clients.set(targetId, { clients.set(targetId, {
client, client,
Page, Page,
@ -211,6 +211,7 @@ const initCDP = async (targetId) => {
Target, Target,
Network, Network,
Emulation, Emulation,
DOM,
}); });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
@ -381,42 +382,75 @@ const getCookie = async (tab, name) => {
// 捕获标签页截图 // 捕获标签页截图
const captureScreenshot = async (tab, options = {}) => { const captureScreenshot = async (tab, options = {}) => {
const target = await searchTarget(tab); const target = await searchTarget(tab);
const { format = "png", quality = 100, fullPage = false, savePath } = options; const {
format = "png",
quality = 100,
savePath,
selector = null,
} = options;
try { try {
const { Page, Emulation } = await initCDP(target.id); const { Page, Emulation, DOM } = await initCDP(target.id);
await DOM.enable();
if (fullPage) { let clip = null;
const metrics = await Page.getLayoutMetrics(); if (selector) {
const width = Math.max( // 获取DOM节点
metrics.contentSize.width, const { root } = await DOM.getDocument();
metrics.layoutViewport.clientWidth, const { nodeId } = await DOM.querySelector({
metrics.visualViewport.clientWidth nodeId: root.nodeId,
); selector: selector,
const height = Math.max(
metrics.contentSize.height,
metrics.layoutViewport.clientHeight,
metrics.visualViewport.clientHeight
);
await Emulation.setDeviceMetricsOverride({
width,
height,
deviceScaleFactor: 1,
mobile: false,
}); });
if (!nodeId) {
throw new Error(`未找到元素: ${selector}`);
} }
const { data } = await Page.captureScreenshot({ // 获取元素的精确四边形坐标
const { quads } = await DOM.getContentQuads({ nodeId });
if (!quads || quads.length === 0) {
throw new Error("无法获取元素位置信息");
}
// 获取布局指标
const { visualViewport } = await Page.getLayoutMetrics();
const { pageX, pageY } = visualViewport;
// 计算边界框
const quad = quads[0];
const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
clip = {
x: Math.round(x - pageX),
y: Math.round(y - pageY),
width: Math.round(width),
height: Math.round(height),
scale: 1,
};
// 确保尺寸不为0
if (clip.width === 0) clip.width = 1;
if (clip.height === 0) clip.height = 1;
}
const screenshotParams = {
format, format,
quality: format === "jpeg" ? quality : undefined, quality: format === "jpeg" ? quality : undefined,
fromSurface: true, fromSurface: true,
captureBeyondViewport: fullPage, captureBeyondViewport: !!selector,
}); };
if (fullPage) { if (clip) {
await Emulation.clearDeviceMetricsOverride(); screenshotParams.clip = clip;
} }
const { data } = await Page.captureScreenshot(screenshotParams);
await DOM.disable();
if (savePath) { if (savePath) {
fs.writeFileSync(savePath, data, "base64"); fs.writeFileSync(savePath, data, "base64");
} }

View File

@ -183,6 +183,7 @@ export const browserCommands = {
value: "quickcomposer.browser.captureScreenshot", value: "quickcomposer.browser.captureScreenshot",
label: "捕获截图", label: "捕获截图",
icon: "screenshot", icon: "screenshot",
isAsync: true,
config: [ config: [
tabConfig, tabConfig,
{ {
@ -191,37 +192,39 @@ export const browserCommands = {
icon: "settings", icon: "settings",
width: 12, width: 12,
options: { options: {
quality: {
label: "质量",
component: "NumberInput",
width: 2,
min: 0,
max: 100,
},
selector: {
label: "指定元素CSS选择器",
component: "VariableInput",
icon: "code",
width: 10,
placeholder: "留空截取可视区域截取整个页面可填body",
options: {
cssSelector: true,
},
},
format: { format: {
label: "格式", label: "格式",
component: "QSelect", component: "QSelect",
icon: "format", width: 2,
width: 4,
options: [ options: [
{ label: "PNG", value: "png" }, { label: "PNG", value: "png" },
{ label: "JPEG", value: "jpeg" }, { label: "JPEG", value: "jpeg" },
{ label: "WebP", value: "webp" }, { label: "WebP", value: "webp" },
], ],
}, },
quality: {
label: "质量",
component: "NumberInput",
icon: "quality",
width: 4,
min: 0,
max: 100,
},
fullPage: {
label: "全屏截图",
component: "CheckButton",
icon: "fullscreen",
width: 4,
},
savePath: { savePath: {
label: "保存路径", label: "保存路径",
component: "VariableInput", component: "VariableInput",
icon: "folder", icon: "folder",
placeholder: "留空则不保存", placeholder: "留空则不保存",
width: 12, width: 10,
options: { options: {
dialog: { dialog: {
type: "save", type: "save",