根因:Chrome '下载前询问保存位置' 设置与 saveAs 冲突。 关闭该设置后下载正常。background 通过 storage.session 传 base64 并下载。
197 lines
8.2 KiB
JavaScript
197 lines
8.2 KiB
JavaScript
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]) });
|
||
});
|
||
});
|