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, concurrency: "8", captureMode: "mixed", captureWidth: null }; const MIN_CAPTURE_WIDTH = 320; const MAX_CAPTURE_WIDTH = 3840; 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.__webToPixsoRunCapture(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 { // Restoring the user's window is best effort only. } }; 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-pixso/${safeTitle}-${Date.now()}.json`; await chrome.downloads.download({ url, filename, saveAs: true }); 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 图片代理" }; } const response = await fetchWithTimeout(url, 12000); if (!response.ok) { return { ok: false, status: response.status, error: `HTTP ${response.status}` }; } 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?.() }); } 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; });