/* 文件: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}`);
}
}