diff --git a/src/main.ts b/src/main.ts index b3d97ec..1f26603 100644 --- a/src/main.ts +++ b/src/main.ts @@ -272,8 +272,10 @@ export default class VitePressExporterPlugin extends Plugin { return content; } - // 尝试捕获 Obsidian 原生 Canvas 渲染(通过临时调整视口大小) - async renderCanvas(canvasFile: TFile): Promise { + // Use Canvas internal API to export (基于 Obsidian export as image 实现) + async renderCanvas( + canvasFile: TFile + ): Promise { const activeLeaf = this.app.workspace.getLeaf(); if (!activeLeaf) { @@ -282,113 +284,219 @@ export default class VitePressExporterPlugin extends Plugin { } await activeLeaf.openFile(canvasFile); - + //@ts-ignore const canvas = activeLeaf.view.canvas; if (!canvas) { console.log("Could not get canvas object from view"); return null; } - // Use Canvas internal API - const imageData = await this.exportCanvasWithInternalAPI(canvas, canvasFile); - if (!imageData) { - console.log("Canvas internal API capture failed"); - return null; - } - console.log("Canvas internal API capture successful!"); - return imageData; + // 选项设置(基于用户要求) + const zoomScale = 1.0; // 缩放比例 1.0 (100%) + const privacyMode = false; // 不启用隐私模式 - } - - // Use Canvas internal API to export - async exportCanvasWithInternalAPI( - canvas: any, - canvasFile: TFile - ): Promise { try { - console.log("=== Using Canvas Internal API ==="); + // 获取系统参数 + const win = window; + //@ts-ignore + const electron = win.electron?.remote; + const webContents = electron?.getCurrentWebContents(); + const zoomLevel = webContents?.getZoomLevel() || 0; + const o = Math.pow(1.2, zoomLevel); // 系统缩放因子 + const a = win.devicePixelRatio || 1; // 设备像素比 + const s = Math.round(a / o * 1e5) / 1e5; // 缩放校正系数 - const { vault } = this.app; + // 获取 Canvas 边界 + const nodes = Array.from(canvas.nodes.values()); + const bbox = this.calculateBBox(nodes); + const c = bbox.maxX - bbox.minX; // 宽度 + const u = bbox.maxY - bbox.minY; // 高度 - // 1. Get viewport bbox + // 计算最大缩放限制(避免超过 16284px) + const h = Math.min(2, 16284 / s / c, 16284 / s / u); + const d = Math.min(zoomScale, h); // 实际使用的缩放比例 + + console.log(`Canvas export: scale=${d}, size=${c}x${u}`); + + // Full canvas 模式 - 使用分块渲染 + const e = canvas.wrapperEl; + + // 准备 Canvas 环境 + this.deselectAll(); + e.addClass("is-screenshotting"); + if (privacyMode) { + e.addClass("is-text-garbled"); + } // @ts-ignore - const bbox = canvas.getViewportBBox(); - console.log("Viewport bbox:", bbox); + canvas.screenshotting = true; + document.body.appendChild(e); - // 2. Calculate content size - const contentWidth = bbox.maxX - bbox.minX; - const contentHeight = bbox.maxY - bbox.minY; - console.log(`Content size: ${contentWidth}x${contentHeight}`); + try { + // 设置 Canvas 位置 + const x = 5 / o; + e.setCssStyles({ + top: `${x}px`, + left: `${x}px`, + bottom: `${x}px`, + right: `${x}px`, + width: "auto", + height: "auto" + }); - // 3. Set zoom = 0 (100%) - // @ts-ignore - const oldZoom = canvas.zoom; - // @ts-ignore - canvas.zoom = 0; - console.log(`Set zoom from ${oldZoom} to 0 (100%)`); + // 计算视口 + const P = e.getBoundingClientRect(); + const L = Math.ceil(P.x * o + 1); + const I = Math.ceil(P.y * o + 1); + const O = Math.floor(P.width * o) - 2; + const F = Math.floor(P.height * o) - 2; - // 4. Request frame - // @ts-ignore - canvas.requestFrame(); - await new Promise(resolve => setTimeout(resolve, 100)); - await new Promise(resolve => requestAnimationFrame(resolve)); + // 设置缩放 + // @ts-ignore + canvas.zoom = Math.log2(d / o); + // @ts-ignore + canvas.tZoom = canvas.zoom; + // @ts-ignore + canvas.viewportChanged = true; + canvas.requestFrame(); + // 等待渲染 + await new Promise(resolve => setTimeout(resolve, 500)); + await new Promise(resolve => requestAnimationFrame(resolve)); - // Generate temp filename (relative path) - const tempFileName = `temp_canvas_${Date.now()}.png`; + // 计算分块数量 + const tileWidth = O / d; + const tileHeight = F / d; + const cols = Math.ceil((bbox.maxX - bbox.minX) / tileWidth); + const rows = Math.ceil((bbox.maxY - bbox.minY) / tileHeight); + const startX = bbox.minX + tileWidth / 2; + const startY = bbox.minY + tileHeight / 2; - // Ensure assets directory exists - await vault.adapter.mkdir(this.settings.assetsPath); + console.log(`Rendering ${cols}x${rows} tiles (${cols * rows} total)`); - // Generate final save path (relative path) - const finalFileName = canvasFile.name.replace(".canvas", ".png"); - const finalPath = `${this.settings.assetsPath}/${finalFileName}`; + // 创建最终 Canvas + const finalCanvas = document.createElement("canvas"); + finalCanvas.width = Math.ceil(c * d * s); + finalCanvas.height = Math.ceil(u * d * s); + const ctx = finalCanvas.getContext("2d"); + if (!ctx) { + throw new Error("Failed to get 2d context"); + } - // Get vault full path - // @ts-ignore - const vaultBasePath = vault.adapter.basePath; - console.log("Vault base path:", vaultBasePath); + let tileIndex = 0; + const totalTiles = cols * rows; - // Build temp file absolute path - const tempFullPath = `${vaultBasePath}/${tempFileName}`; - console.log("Temp full path:", tempFullPath); + // 渲染每个分块 + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + // 设置视口位置 + // @ts-ignore + canvas.x = canvas.tx = startX + col * tileWidth; + // @ts-ignore + canvas.y = canvas.ty = startY + row * tileHeight; + // @ts-ignore + canvas.viewportChanged = true; + canvas.requestFrame(); - // Call takeScreenshot (using absolute path) - // @ts-ignore - await canvas.takeScreenshot(tempFullPath, false); - console.log(`Screenshot saved to ${tempFullPath}`); + await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise(resolve => requestAnimationFrame(resolve)); - // Wait for file write to complete - await new Promise(resolve => setTimeout(resolve, 100)); + // 捕获当前视口 + const capturedData = await webContents.capturePage({ x: L, y: I, width: O, height: F }); - // Read temp file (using relative path, returns ArrayBuffer) - const imageBuffer = await vault.adapter.readBinary(tempFileName); - console.log(`Read imageBuffer, type: ${imageBuffer?.constructor?.name}, byteLength: ${imageBuffer?.byteLength}`); + // 转换为图片并绘制到最终 Canvas + const pngData = capturedData.toPNG(); + const blob = new Blob([pngData], { type: "image/png" }); + const imageUrl = URL.createObjectURL(blob); - // Check if read was successful - if (!imageBuffer || imageBuffer.byteLength === 0) { - console.error("Failed to read temporary file"); - return null; + const img = document.createElement("img"); + await new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = reject; + img.src = imageUrl; + }); + + ctx.drawImage(img, col * O * s, row * F * s); + URL.revokeObjectURL(imageUrl); + + tileIndex++; + console.log(`Rendered tile ${tileIndex}/${totalTiles}`); + } + } + + // 转换为 PNG + const pngBlob = await new Promise((resolve) => finalCanvas.toBlob(resolve, "image/png")); + if (!pngBlob) { + throw new Error("Failed to create PNG blob"); + } + const arrayBuffer = await pngBlob.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + + return uint8Array; + + } finally { + // 清理 + // @ts-ignore + this.app.workspace.activeLeaf?.view?.contentEl?.appendChild(e); + e.removeClass("is-screenshotting"); + e.removeClass("is-text-garbled"); + e.setCssStyles({ + top: "", + left: "", + bottom: "", + right: "", + width: "", + height: "" + }); + // @ts-ignore + canvas.screenshotting = false; + + await new Promise(resolve => requestAnimationFrame(resolve)); + // @ts-ignore + canvas.zoomToFitQueued = true; + // @ts-ignore + canvas.onResize(); } - // Delete temp file - await vault.adapter.remove(tempFileName); - console.log("Removed temporary file"); - - // Write to final location (needs ArrayBuffer, not Uint8Array) - await vault.adapter.writeBinary(finalPath, imageBuffer); - console.log(`Image saved to ${finalPath}`); - - // Return Uint8Array - return new Uint8Array(imageBuffer); - } catch (error) { - console.error("exportCanvasWithInternalAPI failed:", error); + console.error("Canvas render failed:", error); return null; } } + // 计算 Canvas 的边界框 + calculateBBox(nodes: any[]): { minX: number, minY: number, maxX: number, maxY: number } { + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + + for (const node of nodes) { + // @ts-ignore + const x = node.x; + // @ts-ignore + const y = node.y; + // @ts-ignore + const width = node.width; + // @ts-ignore + const height = node.height; + + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x + width); + maxY = Math.max(maxY, y + height); + } + + return { minX, minY, maxX, maxY }; + } + + // 取消选择所有节点 + deselectAll(): void { + // @ts-ignore + const canvas = this.app.workspace.activeLeaf?.view?.canvas; + if (canvas) { + // @ts-ignore + canvas.deselectAll(); + } + } + // 更新内容中的 Canvas 链接 updateCanvasLinksInContent(content: string, canvasFile: TFile, canvasPath: string, imageFileName: string): string { // 获取源文件的目录路径 @@ -445,4 +553,4 @@ export default class VitePressExporterPlugin extends Plugin { return content; } -} \ No newline at end of file +}