Files
ERitors-Scribe-Desktop/electron/database/models/Export.ts
natreex 1a15692e40 Introduce Import and Advanced Export Features
- Added `ImportBookForm` component for importing DOCX files with chapter selection and metadata customization.
- Implemented advanced export options (PDF, DOCX, EPUB) with `ExportSetting` component.
- Developed utility methods for transforming books into exportable formats in `Export.ts`.
- Expanded database models and repositories to support import/export functionality.
- Enhanced localization for import/export flows and updated UI components for improved user experience.
2026-03-11 11:52:25 -04:00

212 lines
8.1 KiB
TypeScript

import {AlignmentType, Document, HeadingLevel, Packer, Paragraph, TextRun} from "docx";
import PDFDocument from "pdfkit";
import JSZip from "jszip";
import {mainStyle} from "./EpubStyle.js";
import Chapter, {ChapterContentData, CompleteChapterContent} from "./Chapter.js";
import {CompleteBookData} from "./Book.js";
import System from "../System.js";
export interface ExportResult {
buffer: Buffer;
fileName: string;
}
export default class Export {
static async transformToDOCX(bookData: CompleteBookData): Promise<ExportResult> {
const bookTitle: string = bookData.title;
const filename: string = `${bookTitle}.docx`;
const docParagraphs: Paragraph[] = [];
docParagraphs.push(
new Paragraph({
children: [
new TextRun({text: bookTitle, bold: true, size: 48}),
],
alignment: AlignmentType.CENTER,
spacing: {after: 400},
})
);
if (bookData.subTitle) {
docParagraphs.push(
new Paragraph({
children: [
new TextRun({text: bookData.subTitle, italics: true, size: 32}),
],
alignment: AlignmentType.CENTER,
spacing: {after: 300},
})
);
}
if (bookData.summary) {
docParagraphs.push(
new Paragraph({
children: [
new TextRun({text: bookData.summary, size: 24, italics: true}),
],
alignment: AlignmentType.JUSTIFIED,
spacing: {after: 400},
})
);
}
const chapters: ChapterContentData[] = Chapter.getChaptersOrSheet(bookData.chapters);
for (const chapter of chapters) {
if (!chapter.content) continue;
docParagraphs.push(
new Paragraph({
text: chapter.title,
heading: HeadingLevel.HEADING_1,
pageBreakBefore: true,
alignment: AlignmentType.CENTER,
spacing: {after: 200},
})
);
const paragraphs: string[] = chapter.content.split(/\r?\n/);
for (const paragraph of paragraphs) {
if (paragraph.trim() === "") continue;
docParagraphs.push(
new Paragraph({
children: [new TextRun({text: paragraph, size: 24})],
alignment: AlignmentType.JUSTIFIED,
spacing: {after: 200},
})
);
}
}
const doc: Document = new Document({
sections: [{children: docParagraphs}],
});
const buffer: Buffer = await Packer.toBuffer(doc) as Buffer;
return {buffer, fileName: filename};
}
static async transformToPDF(bookData: CompleteBookData): Promise<ExportResult> {
const bookTitle: string = bookData.title;
const filename: string = `${bookTitle}.pdf`;
const chunks: Buffer[] = [];
const pdfDoc: PDFKit.PDFDocument = new PDFDocument();
pdfDoc.on('data', (chunk: Buffer): void => {
chunks.push(chunk);
});
pdfDoc.fontSize(20).text(bookTitle, {align: 'center'});
pdfDoc.moveDown();
if (bookData.subTitle && bookData.subTitle.trim() !== '') {
pdfDoc.fontSize(16).text(bookData.subTitle, {align: 'center'});
pdfDoc.moveDown();
}
if (bookData.summary && bookData.summary.trim() !== '') {
pdfDoc.fontSize(12).text(bookData.summary, {align: 'justify'});
pdfDoc.moveDown();
}
const chapters: ChapterContentData[] = Chapter.getChaptersOrSheet(bookData.chapters);
for (const chapter of chapters) {
if (!chapter.content) continue;
pdfDoc.addPage();
pdfDoc.fontSize(16).text(chapter.title, {align: 'center'});
pdfDoc.moveDown();
pdfDoc.fontSize(12).text(chapter.content, {align: 'justify'});
}
pdfDoc.end();
await new Promise<void>((resolve: () => void, reject: (reason: Error) => void) => {
pdfDoc.on('end', resolve);
pdfDoc.on('error', reject);
});
const pdfBuffer: Buffer = Buffer.concat(chunks);
return {buffer: pdfBuffer, fileName: filename};
}
static async transformToEpub(bookData: CompleteBookData): Promise<ExportResult> {
const bookTitle: string = bookData.title;
const bookId: string = bookData.bookId;
const epub: JSZip = new JSZip();
epub.file('mimetype', 'application/epub+zip', {compression: 'STORE'});
epub.file('META-INF/container.xml', `<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
<rootfiles>
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>`);
let contentOpf: string = `<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="ERitors-${bookId}">
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>${bookTitle}${bookData.subTitle ? ' - ' + bookData.subTitle : ''}</dc:title>
<dc:language>fr</dc:language>
<dc:identifier id="ERitors-${bookId}">urn:uuid:${bookId}</dc:identifier>
<dc:creator>${bookData.userInfos.firstName} ${bookData.userInfos.lastName}</dc:creator>
<dc:publisher>ERitors Scribe</dc:publisher>
<meta name="cover" content="cover-image-id" />
</metadata>
<manifest>`;
let spine: string = `<spine toc="toc">`;
const hasRegularChapters: boolean = bookData.chapters.some(
(chapter: CompleteChapterContent): boolean => chapter.order > 0
);
const chaptersToExport: CompleteChapterContent[] = hasRegularChapters
? bookData.chapters.filter((chapter: CompleteChapterContent): boolean => chapter.order > 0)
: bookData.chapters.filter((chapter: CompleteChapterContent): boolean => chapter.order === -1);
for (const chapter of chaptersToExport) {
if (!chapter.content) continue;
const chapterIndex: string = `chapter${chapter.order}`;
const htmlContent: string = Chapter.tipTapToHtml(JSON.parse(chapter.content) as JSON);
const xhtmlPage: string = `<?xml version="1.0" encoding="utf-8"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>${chapter.title}</title>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head>
<body>
${htmlContent}
</body>
</html>`;
epub.file(`OEBPS/${chapterIndex}.xhtml`, xhtmlPage);
contentOpf += `<item id="${chapterIndex}" href="${chapterIndex}.xhtml" media-type="application/xhtml+xml"/>`;
spine += `<itemref idref="${chapterIndex}" linear="yes"/>`;
}
spine += `</spine>`;
contentOpf += `<item id="toc" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
<item id="style" href="styles.css" media-type="text/css"/>`;
contentOpf += spine;
contentOpf += `</package>`;
epub.file('OEBPS/content.opf', contentOpf);
epub.file('OEBPS/styles.css', mainStyle);
if (bookData.coverImage) {
const imageBuffer: Buffer = Buffer.from(bookData.coverImage, 'base64');
epub.file('OEBPS/cover.jpg', imageBuffer);
}
const epubBuffer: Buffer = await epub.generateAsync({type: 'nodebuffer'}) as Buffer;
return {buffer: epubBuffer, fileName: `${bookTitle}.epub`};
}
}