- 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.
212 lines
8.1 KiB
TypeScript
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`};
|
|
}
|
|
}
|