const CAPTURE_FILE = "capture.js"; const RUNNER_FILE = "runner.js"; const SETTINGS_KEY = "webToPixsoSettings"; const DEFAULT_SETTINGS = { useProxy: false, concurrency: "8", captureMode: "mixed", captureWidth: null }; const MIN_CAPTURE_WIDTH = 320; const MAX_CAPTURE_WIDTH = 3840; 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(); } } 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) => { // 采集并缓存数据 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; return { ok: true, actualViewportWidth: data.source?.actualViewportWidth }; })() .then(sendResponse) .catch(error => sendResponse({ ok: false, error: error.message || String(error) })); return true; } // 获取缓存数据 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; } // 下载 PPTX(从 storage 读取 base64) if (message?.type === "WEB_TO_PPT_DOWNLOAD") { (async () => { const stored = await chrome.storage.session.get(['pptxBase64', 'pptxFilename']); if (!stored.pptxBase64) throw new Error('没有待下载的数据'); const dataUrl = 'data:application/vnd.openxmlformats-officedocument.presentationml.presentation;base64,' + stored.pptxBase64; await chrome.downloads.download({ url: dataUrl, filename: stored.pptxFilename }); await chrome.storage.session.remove(['pptxBase64', 'pptxFilename']); return { ok: true }; })() .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]) }); }); });