feat: v2.0 — 插件改造,集成 PPTX 导出

改造内容:
- 改名 Web to Pixso → Web to PPT
- 删除 pixso-plugin/、element-picker.js、popup-panel.js
- 新增 convert-browser.js(浏览器版转化逻辑)
- 新增 lib-pptxgen.js(pptxgenjs 浏览器 bundle)
- 重写 popup.js(触发捕获→转化→下载)
- 重写 background.js(缓存采集数据,支持 WEB_TO_PPT_GET_DATA)
- manifest.json 改名 + 添加 default_popup

流程:popup 点击导出 → background 抓取 DOM → popup 运行转化 → chrome.downloads 下载 PPTX
This commit is contained in:
李进
2026-07-27 13:54:11 +08:00
parent 330f4e96ce
commit e1c8b3d94e
16 changed files with 858 additions and 3214 deletions
+49 -194
View File
@@ -1,7 +1,5 @@
const CAPTURE_FILE = "capture.js";
const RUNNER_FILE = "runner.js";
const POPUP_PANEL_FILE = "popup-panel.js";
const ELEMENT_PICKER_FILE = "element-picker.js";
const SETTINGS_KEY = "webToPixsoSettings";
const DEFAULT_SETTINGS = {
useProxy: false,
@@ -12,6 +10,9 @@ const DEFAULT_SETTINGS = {
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 = {}) {
@@ -43,7 +44,6 @@ 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("浏览器内置页面不支持采集,请切换到普通网页");
}
@@ -60,22 +60,18 @@ async function runCapture(tabId, options) {
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.__webToPixsoRunCapture(captureOptions),
args: [options]
});
if (!result) {
throw new Error("页面没有返回采集结果");
}
return result;
}
@@ -133,9 +129,7 @@ async function prepareCaptureViewport(tab, requestedWidth) {
await chrome.windows.update(tab.windowId, { state: originalState });
}
await delay(250);
} catch {
// Restoring the user's window is best effort only.
}
} catch {}
};
try {
@@ -157,7 +151,7 @@ async function prepareCaptureViewport(tab, requestedWidth) {
}
if (Math.abs(currentViewportWidth - targetWidth) > 2) {
throw new Error(`采集视口未生效:目标 ${targetWidth}px,实际 ${currentViewportWidth}px。请退出全屏或手动放宽浏览器窗口后重试。`);
throw new Error(`采集视口未生效:目标 ${targetWidth}px,实际 ${currentViewportWidth}px`);
}
return {
@@ -203,7 +197,7 @@ async function downloadCapture(data) {
.replace(/[\\/:*?"<>|]+/g, "-")
.replace(/\s+/g, "-")
.slice(0, 64) || "webpage";
const filename = `web-to-pixso/${safeTitle}-${Date.now()}.json`;
const filename = `web-to-ppt/${safeTitle}-${Date.now()}.json`;
await chrome.downloads.download({
url,
@@ -214,206 +208,67 @@ async function downloadCapture(data) {
return filename;
}
async function startCapture(options) {
const settings = normalizeSettings(options);
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
const tab = await getActiveTab();
const data = await captureCurrentTab(tab, settings);
const filename = await downloadCapture(data);
return {
ok: true,
filename,
actualViewportWidth: data.source?.actualViewportWidth,
requestedViewportWidth: data.source?.requestedViewportWidth,
usedTemporaryWindow: Boolean(data.capture?.usedTemporaryWindow)
};
}
async function startCaptureFromSender(options, sender) {
const settings = normalizeSettings(options);
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
const tab = sender?.tab || await getActiveTab();
assertCaptureableTab(tab);
const data = await captureCurrentTab(tab, settings);
const filename = await downloadCapture(data);
return {
ok: true,
filename,
actualViewportWidth: data.source?.actualViewportWidth,
requestedViewportWidth: data.source?.requestedViewportWidth,
usedTemporaryWindow: Boolean(data.capture?.usedTemporaryWindow)
};
}
async function captureClipboardFromSender(options, sender) {
const settings = normalizeSettings(options);
await chrome.storage.local.set({ [SETTINGS_KEY]: settings });
const tab = sender?.tab || await getActiveTab();
assertCaptureableTab(tab);
const data = await captureCurrentTab(tab, settings);
return {
ok: true,
json: JSON.stringify(data, null, 2),
actualViewportWidth: data.source?.actualViewportWidth,
requestedViewportWidth: data.source?.requestedViewportWidth,
usedTemporaryWindow: Boolean(data.capture?.usedTemporaryWindow)
};
}
async function startElementCapture(options, sender) {
const settings = {
...normalizeSettings(options),
captureWidth: null,
selectionId: options?.selectionId,
selectionWidth: Math.max(1, Math.round(Number(options?.selectionWidth || 0))),
captureMode: "mixed"
};
const tab = sender?.tab || await getActiveTab();
assertCaptureableTab(tab);
const data = await runCapture(tab.id, settings);
const filename = await downloadCapture(data);
return {
ok: true,
filename,
actualViewportWidth: data.source?.actualViewportWidth,
requestedViewportWidth: data.source?.requestedViewportWidth,
selectionWidth: data.import?.defaultWidth
};
}
async function fetchWithTimeout(url, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
return await fetch(url, {
signal: controller.signal,
credentials: "include",
cache: "force-cache"
});
} finally {
clearTimeout(timeoutId);
}
}
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);
}
async function proxyFetchAsset(url) {
if (!/^https?:\/\//i.test(url)) {
return { ok: false, error: "仅支持 http/https 图片代理" };
// 消息处理
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 采集并下载 JSON(兼容旧流程)
if (message?.type === "PIXSO_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;
}
const response = await fetchWithTimeout(url, 12000);
if (!response.ok) {
return { ok: false, status: response.status, error: `HTTP ${response.status}` };
// 获取最近一次采集数据(供 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;
}
const contentType = response.headers.get("content-type") || "application/octet-stream";
const buffer = await response.arrayBuffer();
return {
ok: true,
status: response.status,
contentType,
base64: arrayBufferToBase64(buffer)
};
}
async function captureVisibleTab(sender) {
if (!sender?.tab?.windowId || !chrome.tabs?.captureVisibleTab) {
return { ok: false, error: "当前标签页截图不可用" };
}
try {
const dataUrl = await chrome.tabs.captureVisibleTab(sender.tab.windowId, {
format: "png"
});
return { ok: true, dataUrl };
} catch (error) {
return {
ok: false,
error: error.message || String(error),
nonFatal: true
};
}
}
async function showInPagePanel(tab) {
assertCaptureableTab(tab);
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: [ELEMENT_PICKER_FILE, POPUP_PANEL_FILE]
});
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => window.__webToPixsoShowPanel?.()
});
}
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]) });
});
});
chrome.action.onClicked.addListener(tab => {
showInPagePanel(tab).catch(error => {
console.warn("[Web to Pixso] Failed to open panel", error);
});
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message?.type === "PIXSO_CAPTURE_START") {
startCapture(message.options)
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
if (message?.type === "PIXSO_CAPTURE_ELEMENT_START") {
startElementCapture(message.options, sender)
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
if (message?.type === "PIXSO_CAPTURE_CURRENT_TAB") {
startCaptureFromSender(message.options, sender)
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
if (message?.type === "PIXSO_CAPTURE_CLIPBOARD") {
captureClipboardFromSender(message.options, sender)
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
if (message?.type === "PIXSO_CAPTURE_FETCH_ASSET") {
proxyFetchAsset(message.url)
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
if (message?.type === "PIXSO_CAPTURE_VISIBLE_TAB") {
captureVisibleTab(sender)
.then(sendResponse)
.catch(error => sendResponse({ ok: false, error: error.message || String(error) }));
return true;
}
return false;
});
// 点击图标打开 popup(MV3 默认行为)