Files

208 lines
8.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
// 保存 JSON 供调试
try {
const json = JSON.stringify(data, null, 2);
const encoded = btoa(unescape(encodeURIComponent(json)));
const title = data.source?.title || 'debug';
const safe = title.replace(/[\\/:*?"<>|]+/g, '-').slice(0, 32);
chrome.downloads.download({
url: 'data:application/json;base64,' + encoded,
filename: 'web-to-ppt/' + safe + '-' + Date.now() + '.json'
});
} catch(e) {}
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]) });
});
});