Files
html2pptx/web-to-ppt/background.js
T
李进 f7307a0891 refactor: web-to-pixso → web-to-ppt 全面改名
- 目录 web-to-pixso/ → web-to-ppt/
- 所有代码引用:PIXSO → WEB_TO_PPT, webToPixso → webToPPT
- 数据格式:pixso-design-capture → web-to-ppt-capture
- CSS 选择器:__web_to_pixso_ → __web_to_ppt_
- README 重写,移除 Pixso 相关描述
- docs/web-to-pixso-mapping.md → docs/web-to-ppt-mapping.md
2026-07-27 14:12:44 +08:00

275 lines
8.7 KiB
JavaScript

const CAPTURE_FILE = "capture.js";
const RUNNER_FILE = "runner.js";
const SETTINGS_KEY = "webToPPTSettings";
const DEFAULT_SETTINGS = {
useProxy: false,
concurrency: "8",
captureMode: "mixed",
captureWidth: null
};
const MIN_CAPTURE_WIDTH = 320;
const MAX_CAPTURE_WIDTH = 3840;
// 缓存最近一次采集数据,供 popup 获取
let lastCaptureData = null;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
function normalizeSettings(value = {}) {
const concurrency = String(value.concurrency || DEFAULT_SETTINGS.concurrency);
const captureWidth = normalizeCaptureWidth(value.captureWidth, null);
return {
useProxy: Boolean(value.useProxy),
concurrency: ["4", "6", "8", "10", "12", "16", "20", "infinite"].includes(concurrency)
? concurrency
: DEFAULT_SETTINGS.concurrency,
captureMode: value.captureMode === "editable" ? "editable" : "mixed",
captureWidth
};
}
function normalizeCaptureWidth(value, fallback = null) {
const number = Number.parseInt(String(value || "").replace(/\D+/g, ""), 10);
if (!Number.isFinite(number)) return fallback;
return Math.max(MIN_CAPTURE_WIDTH, Math.min(MAX_CAPTURE_WIDTH, number));
}
function definedWindowBounds(bounds = {}) {
return Object.fromEntries(
Object.entries(bounds).filter(([, value]) => Number.isFinite(value))
);
}
function assertCaptureableTab(tab) {
if (!tab?.id || !tab.url) {
throw new Error("没有可采集的当前标签页");
}
if (/^(chrome|edge|about|devtools|chrome-extension):/i.test(tab.url)) {
throw new Error("浏览器内置页面不支持采集,请切换到普通网页");
}
}
async function getActiveTab() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
assertCaptureableTab(tab);
return tab;
}
async function runCapture(tabId, options) {
await chrome.scripting.executeScript({
target: { tabId },
files: [CAPTURE_FILE]
});
await chrome.scripting.executeScript({
target: { tabId },
files: [RUNNER_FILE]
});
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId },
func: captureOptions => window.__webToPPTRunCapture(captureOptions),
args: [options]
});
if (!result) {
throw new Error("页面没有返回采集结果");
}
return result;
}
async function getTabViewportWidth(tabId) {
try {
const [{ result }] = await chrome.scripting.executeScript({
target: { tabId },
func: () => Math.round(window.innerWidth || document.documentElement.clientWidth || 0)
});
return normalizeCaptureWidth(result, null);
} catch {
return null;
}
}
async function prepareCaptureViewport(tab, requestedWidth) {
const targetWidth = normalizeCaptureWidth(requestedWidth, null);
const beforeViewportWidth = await getTabViewportWidth(tab.id);
const noop = async () => {};
if (!targetWidth || !beforeViewportWidth || Math.abs(beforeViewportWidth - targetWidth) <= 2) {
return {
restore: noop,
requestedWidth: targetWidth || beforeViewportWidth,
beforeViewportWidth,
actualViewportWidth: beforeViewportWidth,
resizedWindow: false
};
}
if (!tab.windowId || !chrome.windows?.get || !chrome.windows?.update) {
throw new Error("当前浏览器不支持临时调整采集视口宽度");
}
const originalWindow = await chrome.windows.get(tab.windowId);
const originalState = originalWindow.state || "normal";
const originalBounds = {
left: originalWindow.left,
top: originalWindow.top,
width: originalWindow.width,
height: originalWindow.height
};
const restore = async () => {
try {
if (originalState !== "normal") {
await chrome.windows.update(tab.windowId, { state: "normal" });
await delay(120);
}
const restoreBounds = definedWindowBounds(originalBounds);
if (Object.keys(restoreBounds).length) {
await chrome.windows.update(tab.windowId, restoreBounds);
}
if (originalState !== "normal") {
await delay(120);
await chrome.windows.update(tab.windowId, { state: originalState });
}
await delay(250);
} catch {}
};
try {
if (originalState !== "normal") {
await chrome.windows.update(tab.windowId, { state: "normal" });
await delay(250);
}
let currentViewportWidth = beforeViewportWidth;
let currentWindow = await chrome.windows.get(tab.windowId);
for (let attempt = 0; attempt < 2; attempt += 1) {
const delta = targetWidth - currentViewportWidth;
const nextWidth = Math.max(360, Math.round((currentWindow.width || targetWidth) + delta));
await chrome.windows.update(tab.windowId, { width: nextWidth });
await delay(650);
currentViewportWidth = await getTabViewportWidth(tab.id) || currentViewportWidth;
if (Math.abs(currentViewportWidth - targetWidth) <= 2) break;
currentWindow = await chrome.windows.get(tab.windowId);
}
if (Math.abs(currentViewportWidth - targetWidth) > 2) {
throw new Error(`采集视口未生效:目标 ${targetWidth}px,实际 ${currentViewportWidth}px`);
}
return {
restore,
requestedWidth: targetWidth,
beforeViewportWidth,
actualViewportWidth: currentViewportWidth,
resizedWindow: true
};
} catch (error) {
await restore();
throw error;
}
}
async function captureCurrentTab(tab, settings) {
const viewport = await prepareCaptureViewport(tab, settings.captureWidth);
try {
const data = await runCapture(tab.id, {
...settings,
captureWidth: viewport.requestedWidth
});
data.capture = {
...(data.capture || {}),
resizedWindow: viewport.resizedWindow,
usedTemporaryWindow: viewport.resizedWindow,
requestedWidth: viewport.requestedWidth || data.source?.actualViewportWidth || data.canvas?.width,
beforeViewportWidth: viewport.beforeViewportWidth,
actualViewportWidth: data.source?.actualViewportWidth || viewport.actualViewportWidth
};
return data;
} finally {
await viewport.restore();
}
}
async function downloadCapture(data) {
const json = JSON.stringify(data, null, 2);
const encodedJson = arrayBufferToBase64(new TextEncoder().encode(json));
const url = `data:application/json;charset=utf-8;base64,${encodedJson}`;
const title = data.source?.title || "webpage";
const safeTitle = title
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, "-")
.slice(0, 64) || "webpage";
const filename = `web-to-ppt/${safeTitle}-${Date.now()}.json`;
await chrome.downloads.download({
url,
filename,
saveAs: true
});
return filename;
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
let binary = "";
for (let index = 0; index < bytes.length; index += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
}
return btoa(binary);
}
// 消息处理
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 采集并下载 JSON(兼容旧流程)
if (message?.type === "WEB_TO_PPT_CAPTURE_START") {
(async () => {
const settings = normalizeSettings(message.options);
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
const tab = sender?.tab || await getActiveTab();
assertCaptureableTab(tab);
const data = await captureCurrentTab(tab, settings);
lastCaptureData = data; // 缓存数据
const filename = await downloadCapture(data);
return {
ok: true,
filename,
actualViewportWidth: data.source?.actualViewportWidth,
requestedViewportWidth: data.source?.requestedViewportWidth
};
})()
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
// 获取最近一次采集数据(供 popup 转化用)
if (message?.type === "WEB_TO_PPT_GET_DATA") {
if (lastCaptureData) {
sendResponse({ ok: true, data: lastCaptureData });
} else {
// 如果没有缓存,重新采集
(async () => {
const settings = normalizeSettings({});
const tab = await getActiveTab();
const data = await captureCurrentTab(tab, settings);
lastCaptureData = data;
return { ok: true, data };
})()
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
}
return true;
}
return false;
});
// 安装时初始化设置
chrome.runtime.onInstalled.addListener(() => {
chrome.storage.local.get({ [SETTINGS_KEY]: DEFAULT_SETTINGS }).then(result => {
chrome.storage.local.set({ [SETTINGS_KEY]: normalizeSettings(result[SETTINGS_KEY]) });
});
});
// 点击图标打开 popup(MV3 默认行为)