import { Zip, ZipPassThrough } from "fflate"; import { BaseGenerator } from "./baseGenerator"; import report from "./report"; const enc = new TextEncoder(); /** * Handles epub generation */ export class EpubGenerator extends BaseGenerator { async generate() { this.writer = this.opts.file.getWriter(); this.zip = new Zip(); this.zip.ondata = (error, data, final) => { if(error) { console.error(error); if(this.opts.onerror) this.opts.onerror(error); } if(data) { this.writer.write(data); } if(final) { this.writer.close(); } }; this.hashes = []; for(const chapterI in this.opts.chapters) { const chapter = this.opts.chapters[chapterI]; if(!chapter.links) { let data = await this.getURLs(chapter); chapter.links = data.urls; chapter.hashes = data.hashes; chapter.hash = data.hash; this.hashes.push(...chapter.hashes); } } this.mimetype(); this.container(); this.package(); this.toc(); this.callback(); // signals the template is ready for(const chapterI in this.opts.chapters) { const chapter = this.opts.chapters[chapterI]; if(chapter.number == null || chapter.number == undefined) chapter.number = chapterI; for(const i in chapter.links) { let url = chapter.links[i]; let hash = chapter.hashes[i]; this.callback(chapterI, i, false); const start = performance.now(); const res = await this.fetchImage(url, chapter); const image = new ZipPassThrough("OEBPS/" + hash); this.zip.add(image); const data = new Uint8Array(await res.arrayBuffer()); const end = performance.now() - start; report({ bytes: data.byteLength, cached: res.headers.get("X-Cache") === "HIT", duration: end, success: Math.floor(res.status / 100) === 2, url: url }); image.push(data, true); const textContent = new ZipPassThrough("OEBPS/" + i + ".xhtml"); this.zip.add(textContent); textContent.push(enc.encode(` Page ${i + 1} `), true); this.callback(chapterI, i, true); } } this.zip.end(); this.callback(-1, -1, true); } mimetype() { const mimetype = new ZipPassThrough("mimetype"); this.zip.add(mimetype); mimetype.push(enc.encode("application/epub+zip"), true); } container() { const container = new ZipPassThrough("META-INF/container.xml"); this.zip.add(container); container.push(enc.encode(` `), true); } package() { const opf = new ZipPassThrough("OEBPS/content.opf"); this.zip.add(opf); opf.push(enc.encode(` ${this.opts.title} ${this.opts.language || "en"} ${this.opts.author} ${this.opts.id} Image ${this.opts.updatedAt.toString().split("+")[0]}Z pre-paginated portrait landscape ${this.hashes.map((t, i) => ` `).join("\n")} ${this.hashes.map((t, i) => ` `).join("\n")} ${this.hashes.map((t, i) => ` `).join("\n")} `), true); } toc() { const ncx = new ZipPassThrough("OEBPS/toc.ncx"); this.zip.add(ncx); ncx.push(enc.encode(` ${this.opts.title} ${this.opts.author} ${this.opts.chapters.map((t, i) => t.links.map((link, i) => ` ${this.opts.title} Chapter ${t.number} Page ${i + 1} `)).flat().join("\n")} `), true); } }