@@ -186,6 +186,9 @@ export default class VitePressExporterPlugin extends Plugin {
}
}
}
}
const activeLeaf = this . app . workspace . getLeaf ( ) ;
await activeLeaf . openFile ( sourceFile ) ;
// 复制资源文件到 docs/assets
// 复制资源文件到 docs/assets
for ( const assetPath of assetsToCopy ) {
for ( const assetPath of assetsToCopy ) {
try {
try {
@@ -272,123 +275,226 @@ export default class VitePressExporterPlugin extends Plugin {
return content ;
return content ;
}
}
// 尝试捕获 Obsidian 原生 Canvas 渲染(通过临时调整视口大小)
// Use Canvas internal API to export (基于 Obsidian export as image 实现)
async renderCanvas ( canvasFile : TFile ) : Promise < Uint8Array | null > {
async renderCanvas (
canvasFile : TFile
) : Promise < Uint8Array | null > {
const activeLeaf = this . app . workspace . getLeaf ( ) ;
const activeLeaf = this . app . workspace . getLeaf ( ) ;
if ( ! activeLeaf ) {
console . log ( '❌ No active leaf found' ) ;
return null ;
}
await activeLeaf . openFile ( canvasFile ) ;
await activeLeaf . openFile ( canvasFile ) ;
//@ts-ignore
//@ts-ignore
const canvas = activeLeaf . view . canvas ;
const canvas = activeLeaf . view . canvas ;
if ( ! canvas ) {
if ( ! canvas ) {
console . log ( "Could not get canvas object from view" ) ;
console . log ( "Could not get canvas object from view" ) ;
return null ;
return null ;
}
}
// Use Canvas internal API
const imageData = await this . exportCanvasWithInternalAPI ( canvas , canvasFile ) ;
if ( ! imageData ) {
// 选项设置(基于用户要求)
console . log ( "Canvas internal API capture failed" ) ;
const zoomScale = 1.0 ; // 缩放比例 1.0 (100%)
return null ;
const privacyMode = false ; // 不启用隐私模式
}
console . log ( "Canvas internal API capture successful!" ) ;
return imageData ;
}
// Use Canvas internal API to export
async exportCanvasWithInternalAPI (
canvas : any ,
canvasFile : TFile
) : Promise < Uint8Array | null > {
try {
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 * 1 e5 ) / 1 e5 ; // 缩放校正系数
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
// @ts-ignore
const bbox = canvas . getViewportBBox ( ) ;
canvas . screenshotting = true ;
console . log ( "Viewport bbox:" , bbox ) ;
document . body . appendChild ( e ) ;
// 2. Calculate content size
try {
const contentWidth = bbox . maxX - bbox . minX ;
// 设置 Canvas 位置
const contentHeight = bbox . maxY - bbox . minY ;
const x = 5 / o ;
console . log ( ` Content size: ${ contentWidth } x ${ contentHeight } ` ) ;
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 P = e . getBoundingClientRect ( ) ;
const oldZoom = canvas . zoom ;
const L = Math . ceil ( P . x * o + 1 ) ;
// @ts-ignore
const I = Math . ceil ( P . y * o + 1 ) ;
canvas . zoom = 0 ;
const O = Math . floor ( P . width * o ) - 2 ;
console . log ( ` Set zoom from ${ oldZoom } to 0 (100%) ` ) ;
const F = Math . f loor ( P . height * o ) - 2 ;
// 4. Request frame
// 设置缩放
// @ts-ignore
// @ts-ignore
canvas . requestFrame ( ) ;
canvas . zoom = Math . log2 ( d / o ) ;
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
// @ts-ignore
await new Promise ( resolve = > requestAnimationFrame ( resolve ) ) ;
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
console . log ( ` Rendering ${ cols } x ${ rows } tiles ( ${ cols * rows } total) ` ) ;
await vault . adapter . mkdir ( this . settings . assetsPath ) ;
// Generate final save path (relative path)
// 创建最终 Canvas
const finalFileName = canvasFile . name . replace ( ". canvas" , ".png" );
const finalCanvas = document . c reateElement ( "canvas" ) ;
const finalPath = ` ${ this . settings . assetsPath } / ${ finalFileName } ` ;
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
let tileIndex = 0 ;
// @ts-ignore
const totalTiles = cols * rows ;
const vaultBasePath = vault . adapter . basePath ;
console . log ( "Vault base path:" , vaultBasePath ) ;
// Build temp file absolute path
// 渲染每个分块
const tempFullPath = ` ${ vaultBasePath } / ${ tempFileName } ` ;
for ( let row = 0 ; row < rows ; row ++ ) {
console . log ( "Temp full path:" , tempFullPath ) ;
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)
await new Promise ( resolve = > setTimeout ( resolve , 100 ) ) ;
// @ts-ignore
await new Promise ( resolve = > requestAnimationFrame ( resolve ) ) ;
await canvas . takeScreenshot ( tempFullPath , false ) ;
console . log ( ` Screenshot saved to ${ tempFullPath } ` ) ;
// 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)
// 转换为图片并绘制到最终 Canvas
const imageBuffer = await vault . adapter . readBinary ( tempFileName ) ;
const pngData = capturedData . toPNG ( ) ;
console . log ( ` Read imageBuffer, type: ${ imageBuffer ? . constructor ? . name } , byteLength: ${ imageBuffer ? . byteLength } ` ) ;
const blob = new Blob ( [ pngData ] , { type : "image/png" } ) ;
const imageUrl = URL . createObjectURL ( blob ) ;
// Check if read was successful
const img = document . createElement ( "img" ) ;
if ( ! imageBuffer || imageBuffer . byteLength === 0 ) {
await new Promise < void > ( ( resolve , reject ) = > {
console . error ( "Failed to read temporary file" ) ;
img . onload = ( ) = > resolve ( ) ;
return null ;
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 < Blob | null > ( ( 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 ) {
} catch ( error ) {
console . error ( "exportCanvasWithInternalAPI failed:" , error ) ;
console . error ( "Canvas render failed:" , error ) ;
return null ;
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 链接
// 更新内容中的 Canvas 链接
updateCanvasLinksInContent ( content : string , canvasFile : TFile , canvasPath : string , imageFileName : string ) : string {
updateCanvasLinksInContent ( content : string , canvasFile : TFile , canvasPath : string , imageFileName : string ) : string {
// 获取源文件的目录路径
// 获取源文件的目录路径
@@ -445,4 +551,4 @@ export default class VitePressExporterPlugin extends Plugin {
return content ;
return content ;
}
}
}
}