mirror of
https://github.com/yeban8090/book-smith.git
synced 2026-07-22 05:46:23 +00:00
封面导出成功
This commit is contained in:
parent
bf7e5fb0ec
commit
6edcea4a89
4 changed files with 948 additions and 40 deletions
779
package-lock.json
generated
779
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue