/* 文件:slice-image.ts — 预览页面切图功能:将渲染完的 HTML 页面转为长图,再按比例裁剪为多张 PNG 图片。 */ import { toPng } from 'html-to-image'; import { Notice, TFile } from 'obsidian'; import { NMPSettings } from './settings'; import * as fs from 'fs'; import * as path from 'path'; /** * 解析横竖比例字符串(如 "3:4")为数值 */ function parseAspectRatio(ratio: string): { width: number; height: number } { const parts = ratio.split(':').map(p => parseFloat(p.trim())); if (parts.length === 2 && parts[0] > 0 && parts[1] > 0) { return { width: parts[0], height: parts[1] }; } // 默认 3:4 return { width: 3, height: 4 }; } /** * 从 frontmatter 获取 slug,若不存在则使用文件名(去除扩展名) */ function getSlugFromFile(file: TFile, app: any): string { const cache = app.metadataCache.getFileCache(file); if (cache?.frontmatter?.slug) { return String(cache.frontmatter.slug).trim(); } return file.basename; } /** * 确保目录存在 */ function ensureDir(dirPath: string) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } /** * 将 base64 dataURL 转为 Buffer */ function dataURLToBuffer(dataURL: string): Buffer { const base64 = dataURL.split(',')[1]; return Buffer.from(base64, 'base64'); } /** * 切图主函数 * @param articleElement 预览文章的 HTML 元素(#article-section) * @param file 当前文件 * @param app Obsidian App 实例 */ export async function sliceArticleImage(articleElement: HTMLElement, file: TFile, app: any) { const settings = NMPSettings.getInstance(); const { sliceImageSavePath, sliceImageWidth, sliceImageAspectRatio } = settings; // 解析比例 const ratio = parseAspectRatio(sliceImageAspectRatio); const sliceHeight = Math.round((sliceImageWidth * ratio.height) / ratio.width); // 获取 slug const slug = getSlugFromFile(file, app); new Notice(`开始切图:${slug},宽度=${sliceImageWidth},比例=${sliceImageAspectRatio}`); try { // 1. 保存原始样式 const originalWidth = articleElement.style.width; const originalMaxWidth = articleElement.style.maxWidth; const originalMinWidth = articleElement.style.minWidth; // 2. 临时设置为目标宽度进行渲染 articleElement.style.width = `${sliceImageWidth}px`; articleElement.style.maxWidth = `${sliceImageWidth}px`; articleElement.style.minWidth = `${sliceImageWidth}px`; // 等待样式生效和重排 await new Promise(resolve => setTimeout(resolve, 100)); new Notice(`设置渲染宽度: ${sliceImageWidth}px`); // 3. 生成长图 - 使用实际渲染宽度 new Notice('正在生成长图...'); const longImageDataURL = await toPng(articleElement, { width: sliceImageWidth, pixelRatio: 1, cacheBust: true, }); // 4. 恢复原始样式 articleElement.style.width = originalWidth; articleElement.style.maxWidth = originalMaxWidth; articleElement.style.minWidth = originalMinWidth; // 5. 创建临时 Image 对象以获取长图实际高度 const img = new Image(); await new Promise((resolve, reject) => { img.onload = () => resolve(); img.onerror = reject; img.src = longImageDataURL; }); const fullHeight = img.height; const fullWidth = img.width; new Notice(`长图生成完成:${fullWidth}x${fullHeight}px`); // 3. 保存完整长图 ensureDir(sliceImageSavePath); const longImagePath = path.join(sliceImageSavePath, `${slug}.png`); const longImageBuffer = dataURLToBuffer(longImageDataURL); fs.writeFileSync(longImagePath, new Uint8Array(longImageBuffer)); new Notice(`长图已保存:${longImagePath}`); // 4. 计算需要切多少片 const sliceCount = Math.ceil(fullHeight / sliceHeight); new Notice(`开始切图:共 ${sliceCount} 张,每张 ${sliceImageWidth}x${sliceHeight}px`); // 5. 使用 Canvas 裁剪 const canvas = document.createElement('canvas'); canvas.width = sliceImageWidth; canvas.height = sliceHeight; const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('无法创建 Canvas 上下文'); } for (let i = 0; i < sliceCount; i++) { const yOffset = i * sliceHeight; const actualHeight = Math.min(sliceHeight, fullHeight - yOffset); // 清空画布(处理最后一张可能不足高度的情况,用白色填充) ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, sliceImageWidth, sliceHeight); // 绘制裁剪区域 ctx.drawImage( img, 0, yOffset, fullWidth, actualHeight, // 源区域 0, 0, sliceImageWidth, actualHeight // 目标区域 ); // 导出为 PNG const sliceDataURL = canvas.toDataURL('image/png'); const sliceBuffer = dataURLToBuffer(sliceDataURL); const sliceFilename = `${slug}_${i + 1}.png`; const slicePath = path.join(sliceImageSavePath, sliceFilename); fs.writeFileSync(slicePath, new Uint8Array(sliceBuffer)); new Notice(`已保存:${sliceFilename}`); } new Notice(`✅ 切图完成!共 ${sliceCount} 张图片,保存在:${sliceImageSavePath}`); } catch (error) { console.error('切图失败:', error); new Notice(`❌ 切图失败:${error.message}`); } }