封面导出成功

This commit is contained in:
Yeban8090 2025-07-06 16:15:09 +08:00
parent bf7e5fb0ec
commit 6edcea4a89
4 changed files with 948 additions and 40 deletions

779
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@
"dependencies": {
"@popperjs/core": "^2.11.8",
"docx": "^9.5.0",
"html-to-image": "^1.11.13",
"pdf-lib": "^1.17.1",
"uuid": "^11.1.0"
}

View file

@ -1,5 +1,6 @@
import { App, setIcon, Notice, TFile, MarkdownRenderer } from 'obsidian';
import { BookManager } from "../services/BookManager";
import * as htmlToImage from 'html-to-image';
import { Book, CoverSettings, ChapterNode } from "../types/book";
import { ExportService } from "../services/ExportService";
import { i18n } from "../i18n/i18n";
@ -17,9 +18,10 @@ export interface TypographySettings {
margin: string;
templateId: string;
themeId: string;
coverSettings?: CoverSettings; // 封面设置
showCover: boolean; // 是否显示封面
bookSize: string; // 新增:书籍开本大小
coverSettings?: CoverSettings;
showCover: boolean;
bookSize: string;
coverImageData?: string; // 新增:封面图片的 base64 数据
}
export class TypographyView {
@ -594,11 +596,17 @@ export class TypographyView {
}
// 获取排版设置
private getTypographySettings(): TypographySettings {
private async getTypographySettings(): Promise<TypographySettings> {
// 获取封面开关状态
const coverToggle = this.parentEl.querySelector('.cover-toggle-input') as HTMLInputElement;
const showCover = coverToggle ? coverToggle.checked : true;
// 如果显示封面,转换封面为图片
let coverImageData: string | undefined;
if (showCover && this.coverSettings) {
coverImageData = (await this.convertCoverToImage()) || undefined;
}
// 获取字体系列
const fontFamily = (this.customFontSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'default';
@ -614,7 +622,8 @@ export class TypographyView {
themeId: (this.customThemeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'default',
coverSettings: this.coverSettings || undefined,
showCover: showCover,
bookSize: (this.customBookSizeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'a4'
bookSize: (this.customBookSizeSelect?.querySelector('.book-smith-select') as HTMLElement)?.getAttribute('data-value') || 'a4',
coverImageData: coverImageData // 新增封面图片数据
};
}
@ -676,7 +685,10 @@ export class TypographyView {
const settings = this.getTypographySettings();
// 更新预览样式
this.updatePreviewStyle(settings);
// 等待 settings Promise 解析完成后再更新样式
settings.then(resolvedSettings => {
this.updatePreviewStyle(resolvedSettings);
});
// 渲染所有内容
await this.renderAllContent(this.previewElement);
@ -909,7 +921,6 @@ export class TypographyView {
// 应用开本大小样式到预览元素
this.applyBookSizeToPreview(this.coverPreviewElement, coverSettings.bookSize || 'A4');
console.log('使用封面配置:', coverSettings);
const contentContainer = this.coverManager.applyCoverStyles(this.coverPreviewElement, coverSettings);
// 添加书籍信息
@ -990,6 +1001,44 @@ export class TypographyView {
element.style.height = 'auto';
}
// 新增方法:将封面转换为图片
private async convertCoverToImage(): Promise<string | null> {
if (!this.coverPreviewElement || !this.coverSettings) {
return null;
}
try {
// 确保浏览器完成重绘并等待资源加载
await new Promise(resolve => setTimeout(resolve, 300));
// 配置导出选项
const exportConfig = {
quality: 1,
pixelRatio: 4, // 提高分辨率
backgroundColor: '#333333', // 默认背景色
style: {
transform: 'scale(1)',
transformOrigin: 'top left'
}
};
try {
// 首选方法:直接转换为 DataURL
const dataUrl = await htmlToImage.toPng(this.coverPreviewElement, exportConfig);
return dataUrl;
} catch (err) {
console.warn('toPng 失败,尝试备用方法', err);
// 备用方法:使用 toCanvas 然后转换为 DataURL
const canvas = await htmlToImage.toCanvas(this.coverPreviewElement, exportConfig);
return canvas.toDataURL('image/png', 0.9);
}
} catch (error) {
console.error('封面转图片失败:', error);
return null;
}
}
// 导出功能
private async exportWithTypography() {
@ -1025,7 +1074,7 @@ export class TypographyView {
if (format !== "txt") {
useTypography = true;
typographySettings = this.getTypographySettings();
typographySettings = await this.getTypographySettings(); // 改为 await
if (this.previewElement) {
htmlContent = this.previewElement.cloneNode(true) as HTMLElement;
}

View file

@ -292,38 +292,131 @@ export class PdfExportStrategy implements ExportStrategy {
const bodyPdfBuffer = await win.webContents.printToPDF(printOptions);
win.close();
// 替换第295-325行的代码
const PDFLib = await import('pdf-lib');
const coverDoc = await PDFLib.PDFDocument.create();
const page = coverDoc.addPage();
const size = this.bookSizeMap[typographySettings.bookSize || "A4"];
page.setSize(size.width, size.height);
page.drawRectangle({ x: 0, y: 0, width: page.getWidth(), height: page.getHeight(), color: PDFLib.rgb(0, 0, 0) });
page.drawText("Book", {
x: (page.getWidth() - 10) / 2,
y: page.getHeight() - 300, // 距离顶部100
size: 30,
color: PDFLib.rgb(1, 1, 1),
});
const coverBuffer = await coverDoc.save();
// 检查是否有封面图片数据
if (typographySettings.showCover && typographySettings.coverImageData) {
// 创建封面页
const coverDoc = await PDFLib.PDFDocument.create();
const page = coverDoc.addPage();
const size = this.bookSizeMap[typographySettings.bookSize || "A4"];
page.setSize(size.width, size.height);
const finalPdf = await PDFLib.PDFDocument.create();
const [coverPage] = await finalPdf.copyPages(await PDFLib.PDFDocument.load(coverBuffer), [0]);
finalPdf.addPage(coverPage);
try {
// 处理base64图片数据
const base64Data = typographySettings.coverImageData.split(',')[1]; // 移除 "data:image/xxx;base64," 前缀
const imageBytes = Uint8Array.from(atob(base64Data), c => c.charCodeAt(0));
const bodyDoc = await PDFLib.PDFDocument.load(bodyPdfBuffer);
const bodyPages = await finalPdf.copyPages(bodyDoc, bodyDoc.getPageIndices());
bodyPages.forEach(p => finalPdf.addPage(p));
// 根据图片类型嵌入图片
let coverImage;
if (typographySettings.coverImageData.includes('data:image/png')) {
coverImage = await coverDoc.embedPng(imageBytes);
} else if (typographySettings.coverImageData.includes('data:image/jpeg') || typographySettings.coverImageData.includes('data:image/jpg')) {
coverImage = await coverDoc.embedJpg(imageBytes);
} else {
// 默认尝试PNG格式
coverImage = await coverDoc.embedPng(imageBytes);
}
const finalBuffer = await finalPdf.save();
// 绘制封面图片
page.drawImage(coverImage, {
x: 0,
y: 0,
width: page.getWidth(),
height: page.getHeight(),
});
} catch (error) {
console.error('封面图片处理失败,使用默认封面:', error);
// 如果图片处理失败,绘制默认封面
page.drawRectangle({
x: 0,
y: 0,
width: page.getWidth(),
height: page.getHeight(),
color: PDFLib.rgb(0.2, 0.2, 0.2)
});
page.drawText("Book", {
x: page.getWidth() / 2 - 50,
y: page.getHeight() / 2,
size: 30,
color: PDFLib.rgb(1, 1, 1),
});
}
const filePath = await this.getOutputFile(book.basic.title);
if (!filePath) return "cancelled";
await fs.writeFile(filePath, finalBuffer);
const coverBuffer = await coverDoc.save();
return filePath;
// 合并封面和内容PDF
const finalPdf = await PDFLib.PDFDocument.create();
const [coverPage] = await finalPdf.copyPages(await PDFLib.PDFDocument.load(coverBuffer), [0]);
finalPdf.addPage(coverPage);
const bodyDoc = await PDFLib.PDFDocument.load(bodyPdfBuffer);
const bodyPages = await finalPdf.copyPages(bodyDoc, bodyDoc.getPageIndices());
bodyPages.forEach(p => finalPdf.addPage(p));
const finalBuffer = await finalPdf.save();
const filePath = await this.getOutputFile(book.basic.title);
if (!filePath) return "cancelled";
await fs.writeFile(filePath, finalBuffer);
return filePath;
} else {
// 如果没有封面设置或不显示封面,创建简单的文本封面
const coverDoc = await PDFLib.PDFDocument.create();
const page = coverDoc.addPage();
const size = this.bookSizeMap[typographySettings.bookSize || "A4"];
page.setSize(size.width, size.height);
// 绘制默认封面
page.drawRectangle({
x: 0,
y: 0,
width: page.getWidth(),
height: page.getHeight(),
color: PDFLib.rgb(0.1, 0.1, 0.1)
});
// 添加书名
const titleText = "Book";
page.drawText(titleText, {
x: page.getWidth() / 2 - (titleText.length * 10),
y: page.getHeight() / 2 + 50,
size: 24,
color: PDFLib.rgb(1, 1, 1),
});
// 添加作者信息
if (book.basic.author && book.basic.author.length > 0) {
const authorText = book.basic.author.join(', ');
page.drawText(authorText, {
x: page.getWidth() / 2 - (authorText.length * 6),
y: page.getHeight() / 2 - 50,
size: 16,
color: PDFLib.rgb(0.8, 0.8, 0.8),
});
}
const coverBuffer = await coverDoc.save();
// 合并封面和内容PDF
const finalPdf = await PDFLib.PDFDocument.create();
const [coverPage] = await finalPdf.copyPages(await PDFLib.PDFDocument.load(coverBuffer), [0]);
finalPdf.addPage(coverPage);
const bodyDoc = await PDFLib.PDFDocument.load(bodyPdfBuffer);
const bodyPages = await finalPdf.copyPages(bodyDoc, bodyDoc.getPageIndices());
bodyPages.forEach(p => finalPdf.addPage(p));
const finalBuffer = await finalPdf.save();
const filePath = await this.getOutputFile(book.basic.title);
if (!filePath) return "cancelled";
await fs.writeFile(filePath, finalBuffer);
return filePath;
}
} catch (err: any) {
console.error("PDF导出错误:", err);
throw new Error(`PDF导出失败: ${err.message}`);
@ -333,7 +426,7 @@ export class PdfExportStrategy implements ExportStrategy {
private static async processImages(container: HTMLElement): Promise<void> {
const images = container.querySelectorAll('img');
const imageArray = Array.from(images);
for (const img of imageArray) {
try {
const response = await fetch(img.src);