Skip to content

Commit f7295e5

Browse files
Larson-Loganren-lv
authored andcommitted
added support for azw3 -> epub conversion
1 parent 47ee303 commit f7295e5

4 files changed

Lines changed: 1433 additions & 0 deletions

File tree

src/CommonFormats.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,27 @@ const CommonFormats = {
214214
"application/pdf",
215215
Category.DOCUMENT
216216
),
217+
EPUB: new FormatDefinition(
218+
"Electronic Publication",
219+
"epub",
220+
"epub",
221+
"application/epub+zip",
222+
Category.DOCUMENT
223+
),
224+
AZW3: new FormatDefinition(
225+
"Amazon Kindle Format 8",
226+
"azw3",
227+
"azw3",
228+
"application/vnd.amazon.mobi8-ebook",
229+
Category.DOCUMENT
230+
),
231+
MOBI: new FormatDefinition(
232+
"Mobipocket e-book",
233+
"mobi",
234+
"mobi",
235+
"application/x-mobipocket-ebook",
236+
Category.DOCUMENT
237+
),
217238
// documents - Microsoft Office
218239
DOCX: new FormatDefinition(
219240
"WordprocessingML Document",

src/handlers/azw3.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts";
2+
import CommonFormats from "src/CommonFormats.ts";
3+
import JSZip from "jszip";
4+
import pako from "pako";
5+
import { isMOBI, MOBI } from "./azw3/mobi.js";
6+
7+
class azw3Handler implements FormatHandler {
8+
9+
public name: string = "azw3";
10+
public supportedFormats?: FileFormat[];
11+
public ready: boolean = false;
12+
13+
async init () {
14+
this.supportedFormats = [
15+
CommonFormats.AZW3.supported("azw3", true, false),
16+
CommonFormats.MOBI.supported("mobi", true, false),
17+
CommonFormats.EPUB.supported("epub", false, true)
18+
];
19+
this.ready = true;
20+
}
21+
22+
async doConvert (
23+
inputFiles: FileData[],
24+
inputFormat: FileFormat,
25+
outputFormat: FileFormat
26+
): Promise<FileData[]> {
27+
const outputFiles: FileData[] = [];
28+
for (const inputFile of inputFiles) {
29+
const blob = new Blob([new Uint8Array(inputFile.bytes)]);
30+
const valid = await isMOBI(blob);
31+
if (!valid) throw new Error("Invalid MOBI/AZW3 file.");
32+
33+
const book = await new MOBI({ unzlib: pako.inflate }).open(blob);
34+
const metadata = book.metadata;
35+
36+
const zip = new JSZip();
37+
zip.file("mimetype", "application/epub+zip");
38+
39+
const metaInf = zip.folder("META-INF")!;
40+
metaInf.file("container.xml", `<?xml version="1.0" encoding="UTF-8"?>
41+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
42+
<rootfiles>
43+
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
44+
</rootfiles>
45+
</container>`);
46+
47+
const oebps = zip.folder("OEBPS")!;
48+
const manifest: string[] = [];
49+
const spine: string[] = [];
50+
51+
const serializer = new XMLSerializer();
52+
53+
let itemIndex = 0;
54+
55+
for (let i = 0; i < book.sections.length; i++) {
56+
const section = book.sections[i];
57+
const isLinear = section.linear !== 'no';
58+
59+
try {
60+
const doc = await section.createDocument?.() as Document;
61+
if (!doc) continue;
62+
63+
// Images handling
64+
for (const img of Array.from(doc.querySelectorAll('img'))) {
65+
const recindex = img.getAttribute('recindex');
66+
let b64 = null;
67+
let type = "image/jpeg";
68+
69+
if (recindex != null) {
70+
try {
71+
const buf = await book.mobi.loadResource(Number(recindex) - 1);
72+
const uint8 = new Uint8Array(buf as ArrayBuffer);
73+
let binary = '';
74+
for (let j = 0; j < uint8.byteLength; j++) {
75+
binary += String.fromCharCode(uint8[j]);
76+
}
77+
b64 = btoa(binary);
78+
} catch (e) {
79+
console.warn(e);
80+
}
81+
} else {
82+
const src = img.getAttribute('src');
83+
if (src && src.startsWith('kindle:embed:')) {
84+
try {
85+
const [b, inline] = await (book as any).loadResourceBlob(src);
86+
if (b) {
87+
type = b.type;
88+
const arrayBuffer = await b.arrayBuffer();
89+
const uint8 = new Uint8Array(arrayBuffer);
90+
let binary = '';
91+
for (let j = 0; j < uint8.byteLength; j++) {
92+
binary += String.fromCharCode(uint8[j]);
93+
}
94+
b64 = btoa(binary);
95+
}
96+
} catch (e) {
97+
console.warn(e);
98+
}
99+
}
100+
}
101+
102+
if (b64) {
103+
img.src = `data:${type};base64,` + b64;
104+
}
105+
}
106+
107+
const html = `<?xml version="1.0" encoding="utf-8"?>\n<!DOCTYPE html>\n` + serializer.serializeToString(doc);
108+
const filename = `part${itemIndex}.xhtml`;
109+
oebps.file(filename, html);
110+
111+
manifest.push(`<item id="item${itemIndex}" href="${filename}" media-type="application/xhtml+xml" />`);
112+
spine.push(`<itemref idref="item${itemIndex}" ${isLinear ? '' : 'linear="no" '}/>`);
113+
itemIndex++;
114+
115+
} catch (e) {
116+
console.warn("Failed section", e);
117+
}
118+
}
119+
120+
// Create content.opf
121+
const opf = `<?xml version="1.0" encoding="UTF-8"?>
122+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid">
123+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
124+
<dc:title>${metadata?.title || "Unknown Book"}</dc:title>
125+
<dc:language>${metadata?.language || "en"}</dc:language>
126+
<dc:identifier id="uid">${metadata?.identifier || Date.now()}</dc:identifier>
127+
${metadata?.author ? metadata.author.map((a: string) => `<dc:creator>${a}</dc:creator>`).join('') : ''}
128+
</metadata>
129+
<manifest>
130+
${manifest.join('\\n ')}
131+
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
132+
</manifest>
133+
<spine toc="ncx">
134+
${spine.join('\\n ')}
135+
</spine>
136+
</package>`;
137+
138+
oebps.file("content.opf", opf);
139+
140+
// Create dummy toc.ncx
141+
const ncx = `<?xml version="1.0" encoding="UTF-8"?>
142+
<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">
143+
<head>
144+
<meta name="dtb:uid" content="${metadata?.identifier || Date.now()}"/>
145+
<meta name="dtb:depth" content="1"/>
146+
<meta name="dtb:totalPageCount" content="0"/>
147+
<meta name="dtb:maxPageNumber" content="0"/>
148+
</head>
149+
<docTitle>
150+
<text>${metadata?.title || "Unknown Book"}</text>
151+
</docTitle>
152+
<navMap>
153+
<navPoint id="navPoint-1" playOrder="1">
154+
<navLabel><text>Start</text></navLabel>
155+
<content src="part0.html"/>
156+
</navPoint>
157+
</navMap>
158+
</ncx>`;
159+
oebps.file("toc.ncx", ncx);
160+
161+
const epubBlob = await zip.generateAsync({ type: "uint8array" });
162+
const outputName = inputFile.name.split(".").slice(0, -1).join(".") + ".epub";
163+
outputFiles.push({
164+
name: outputName,
165+
bytes: epubBlob
166+
});
167+
}
168+
return outputFiles;
169+
}
170+
}
171+
172+
export default azw3Handler;

0 commit comments

Comments
 (0)