mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-30 18:48:55 +01:00
451 lines
11 KiB
JavaScript
451 lines
11 KiB
JavaScript
/*
|
|
This is borrowed from koodo-reader https://github.com/troyeguo/koodo-reader/tree/master/src
|
|
*/
|
|
|
|
function ab2str(buf) {
|
|
if (buf instanceof ArrayBuffer) {
|
|
buf = new Uint8Array(buf);
|
|
}
|
|
return new TextDecoder("utf-8").decode(buf);
|
|
}
|
|
|
|
var domParser = new DOMParser();
|
|
|
|
class Buffer {
|
|
capacity;
|
|
fragment_list;
|
|
imageArray;
|
|
cur_fragment;
|
|
constructor(capacity) {
|
|
this.capacity = capacity;
|
|
this.fragment_list = [];
|
|
this.imageArray = [];
|
|
this.cur_fragment = new Fragment(capacity);
|
|
this.fragment_list.push(this.cur_fragment);
|
|
}
|
|
write(byte) {
|
|
var result = this.cur_fragment.write(byte);
|
|
if (!result) {
|
|
this.cur_fragment = new Fragment(this.capacity);
|
|
this.fragment_list.push(this.cur_fragment);
|
|
this.cur_fragment.write(byte);
|
|
}
|
|
}
|
|
get(idx) {
|
|
var fi = 0;
|
|
while (fi < this.fragment_list.length) {
|
|
var frag = this.fragment_list[fi];
|
|
if (idx < frag.size) {
|
|
return frag.get(idx);
|
|
}
|
|
idx -= frag.size;
|
|
fi += 1;
|
|
}
|
|
return null;
|
|
}
|
|
size() {
|
|
var s = 0;
|
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
|
s += this.fragment_list[i].size;
|
|
}
|
|
return s;
|
|
}
|
|
shrink() {
|
|
var total_buffer = new Uint8Array(this.size());
|
|
var offset = 0;
|
|
for (var i = 0; i < this.fragment_list.length; i++) {
|
|
var frag = this.fragment_list[i];
|
|
if (frag.full()) {
|
|
total_buffer.set(frag.buffer, offset);
|
|
} else {
|
|
total_buffer.set(frag.buffer.slice(0, frag.size), offset);
|
|
}
|
|
offset += frag.size;
|
|
}
|
|
return total_buffer;
|
|
}
|
|
}
|
|
|
|
var copagesne_uint8array = function (buffers) {
|
|
var total_size = 0;
|
|
for (let i = 0; i < buffers.length; i++) {
|
|
var buffer = buffers[i];
|
|
total_size += buffer.length;
|
|
}
|
|
var total_buffer = new Uint8Array(total_size);
|
|
var offset = 0;
|
|
for (let i = 0; i < buffers.length; i++) {
|
|
buffer = buffers[i];
|
|
total_buffer.set(buffer, offset);
|
|
offset += buffer.length;
|
|
}
|
|
return total_buffer;
|
|
};
|
|
|
|
class Fragment {
|
|
buffer;
|
|
capacity;
|
|
size;
|
|
constructor(capacity) {
|
|
this.buffer = new Uint8Array(capacity);
|
|
this.capacity = capacity;
|
|
this.size = 0;
|
|
}
|
|
|
|
write(byte) {
|
|
if (this.size >= this.capacity) {
|
|
return false;
|
|
}
|
|
this.buffer[this.size] = byte;
|
|
this.size += 1;
|
|
return true;
|
|
}
|
|
full() {
|
|
return this.size === this.capacity;
|
|
}
|
|
get(idx) {
|
|
return this.buffer[idx];
|
|
}
|
|
}
|
|
|
|
var uncompression_lz77 = function (data) {
|
|
var length = data.length;
|
|
var offset = 0; // Current offset into data
|
|
var buffer = new Buffer(data.length);
|
|
|
|
while (offset < length) {
|
|
var char = data[offset];
|
|
offset += 1;
|
|
|
|
if (char === 0) {
|
|
buffer.write(char);
|
|
} else if (char <= 8) {
|
|
for (var i = offset; i < offset + char; i++) {
|
|
buffer.write(data[i]);
|
|
}
|
|
offset += char;
|
|
} else if (char <= 0x7f) {
|
|
buffer.write(char);
|
|
} else if (char <= 0xbf) {
|
|
var next = data[offset];
|
|
offset += 1;
|
|
var distance = (((char << 8) | next) >> 3) & 0x7ff;
|
|
var lz_length = (next & 0x7) + 3;
|
|
|
|
var buffer_size = buffer.size();
|
|
for (let i = 0; i < lz_length; i++) {
|
|
buffer.write(buffer.get(buffer_size - distance));
|
|
buffer_size += 1;
|
|
}
|
|
} else {
|
|
buffer.write(32);
|
|
buffer.write(char ^ 0x80);
|
|
}
|
|
}
|
|
return buffer;
|
|
};
|
|
|
|
class MobiFile {
|
|
view;
|
|
buffer;
|
|
offset;
|
|
header;
|
|
palm_header;
|
|
mobi_header;
|
|
reclist;
|
|
constructor(data) {
|
|
this.view = new DataView(data);
|
|
this.buffer = this.view.buffer;
|
|
this.offset = 0;
|
|
this.header = null;
|
|
}
|
|
|
|
parse() { }
|
|
|
|
getUint8() {
|
|
var v = this.view.getUint8(this.offset);
|
|
this.offset += 1;
|
|
return v;
|
|
}
|
|
|
|
getUint16() {
|
|
var v = this.view.getUint16(this.offset);
|
|
this.offset += 2;
|
|
return v;
|
|
}
|
|
|
|
getUint32() {
|
|
var v = this.view.getUint32(this.offset);
|
|
this.offset += 4;
|
|
return v;
|
|
}
|
|
|
|
getStr(size) {
|
|
var v = ab2str(this.buffer.slice(this.offset, this.offset + size));
|
|
this.offset += size;
|
|
return v;
|
|
}
|
|
|
|
skip(size) {
|
|
this.offset += size;
|
|
}
|
|
|
|
setoffset(_of) {
|
|
this.offset = _of;
|
|
}
|
|
|
|
get_record_extrasize(data, flags) {
|
|
var pos = data.length - 1;
|
|
var extra = 0;
|
|
for (var i = 15; i > 0; i--) {
|
|
if (flags & (1 << i)) {
|
|
var res = this.buffer_get_varlen(data, pos);
|
|
var size = res[0];
|
|
var l = res[1];
|
|
pos = res[2];
|
|
pos -= size - l;
|
|
extra += size;
|
|
}
|
|
}
|
|
if (flags & 1) {
|
|
var a = data[pos];
|
|
extra += (a & 0x3) + 1;
|
|
}
|
|
return extra;
|
|
}
|
|
|
|
// data should be uint8array
|
|
buffer_get_varlen(data, pos) {
|
|
var l = 0;
|
|
var size = 0;
|
|
var byte_count = 0;
|
|
var mask = 0x7f;
|
|
var stop_flag = 0x80;
|
|
var shift = 0;
|
|
for (var i = 0; ; i++) {
|
|
var byte = data[pos];
|
|
size |= (byte & mask) << shift;
|
|
shift += 7;
|
|
l += 1;
|
|
byte_count += 1;
|
|
pos -= 1;
|
|
|
|
var to_stop = byte & stop_flag;
|
|
if (byte_count >= 4 || to_stop > 0) {
|
|
break;
|
|
}
|
|
}
|
|
return [size, l, pos];
|
|
}
|
|
// 读出文本内容
|
|
read_text() {
|
|
var text_end = this.palm_header.record_count;
|
|
var buffers = [];
|
|
for (var i = 1; i <= text_end; i++) {
|
|
buffers.push(this.read_text_record(i));
|
|
}
|
|
var all = copagesne_uint8array(buffers);
|
|
return ab2str(all);
|
|
}
|
|
|
|
read_text_record(i) {
|
|
var flags = this.mobi_header.extra_flags;
|
|
var begin = this.reclist[i].offset;
|
|
var end = this.reclist[i + 1].offset;
|
|
|
|
var data = new Uint8Array(this.buffer.slice(begin, end));
|
|
var ex = this.get_record_extrasize(data, flags);
|
|
|
|
data = new Uint8Array(this.buffer.slice(begin, end - ex));
|
|
if (this.palm_header.compression === 2) {
|
|
var buffer = uncompression_lz77(data);
|
|
return buffer.shrink();
|
|
} else {
|
|
return data;
|
|
}
|
|
}
|
|
// 从buffer中读出image
|
|
read_image(idx) {
|
|
var first_image_idx = this.mobi_header.first_image_idx;
|
|
var begin = this.reclist[first_image_idx + idx].offset;
|
|
var end = this.reclist[first_image_idx + idx + 1].offset;
|
|
var data = new Uint8Array(this.buffer.slice(begin, end));
|
|
return new Blob([data.buffer]);
|
|
}
|
|
|
|
load() {
|
|
this.header = this.load_pdbheader();
|
|
this.reclist = this.load_reclist();
|
|
this.load_record0();
|
|
}
|
|
|
|
load_pdbheader() {
|
|
var header = {};
|
|
header.name = this.getStr(32);
|
|
header.attr = this.getUint16();
|
|
header.version = this.getUint16();
|
|
header.ctime = this.getUint32();
|
|
header.mtime = this.getUint32();
|
|
header.btime = this.getUint32();
|
|
header.mod_num = this.getUint32();
|
|
header.appinfo_offset = this.getUint32();
|
|
header.sortinfo_offset = this.getUint32();
|
|
header.type = this.getStr(4);
|
|
header.creator = this.getStr(4);
|
|
header.uid = this.getUint32();
|
|
header.next_rec = this.getUint32();
|
|
header.record_num = this.getUint16();
|
|
return header;
|
|
}
|
|
|
|
load_reclist() {
|
|
var reclist = [];
|
|
for (var i = 0; i < this.header.record_num; i++) {
|
|
var record = {};
|
|
record.offset = this.getUint32();
|
|
// TODO(zz) change
|
|
record.attr = this.getUint32();
|
|
reclist.push(record);
|
|
}
|
|
return reclist;
|
|
}
|
|
load_record0() {
|
|
this.palm_header = this.load_record0_header();
|
|
this.mobi_header = this.load_mobi_header();
|
|
}
|
|
|
|
load_record0_header() {
|
|
var p_header = {};
|
|
var first_record = this.reclist[0];
|
|
this.setoffset(first_record.offset);
|
|
|
|
p_header.compression = this.getUint16();
|
|
this.skip(2);
|
|
p_header.text_length = this.getUint32();
|
|
p_header.record_count = this.getUint16();
|
|
p_header.record_size = this.getUint16();
|
|
p_header.encryption_type = this.getUint16();
|
|
this.skip(2);
|
|
|
|
return p_header;
|
|
}
|
|
|
|
load_mobi_header() {
|
|
var mobi_header = {};
|
|
|
|
var start_offset = this.offset;
|
|
|
|
mobi_header.identifier = this.getUint32();
|
|
mobi_header.header_length = this.getUint32();
|
|
mobi_header.mobi_type = this.getUint32();
|
|
mobi_header.text_encoding = this.getUint32();
|
|
mobi_header.uid = this.getUint32();
|
|
mobi_header.generator_version = this.getUint32();
|
|
|
|
this.skip(40);
|
|
|
|
mobi_header.first_nonbook_index = this.getUint32();
|
|
mobi_header.full_name_offset = this.getUint32();
|
|
mobi_header.full_name_length = this.getUint32();
|
|
|
|
mobi_header.language = this.getUint32();
|
|
mobi_header.input_language = this.getUint32();
|
|
mobi_header.output_language = this.getUint32();
|
|
mobi_header.min_version = this.getUint32();
|
|
mobi_header.first_image_idx = this.getUint32();
|
|
|
|
mobi_header.huff_rec_index = this.getUint32();
|
|
mobi_header.huff_rec_count = this.getUint32();
|
|
mobi_header.datp_rec_index = this.getUint32();
|
|
mobi_header.datp_rec_count = this.getUint32();
|
|
|
|
mobi_header.exth_flags = this.getUint32();
|
|
|
|
this.skip(36);
|
|
|
|
mobi_header.drm_offset = this.getUint32();
|
|
mobi_header.drm_count = this.getUint32();
|
|
mobi_header.drm_size = this.getUint32();
|
|
mobi_header.drm_flags = this.getUint32();
|
|
|
|
this.skip(8);
|
|
|
|
// TODO (zz) fdst_index
|
|
this.skip(4);
|
|
|
|
this.skip(46);
|
|
|
|
mobi_header.extra_flags = this.getUint16();
|
|
|
|
this.setoffset(start_offset + mobi_header.header_length);
|
|
|
|
return mobi_header;
|
|
}
|
|
load_exth_header() {
|
|
// TODO
|
|
return {};
|
|
}
|
|
extractContent(s) {
|
|
var span = document.createElement("span");
|
|
span.innerHTML = s;
|
|
return span.textContent || span.innerText;
|
|
}
|
|
render(isElectron = false) {
|
|
return new Promise((resolve, reject) => {
|
|
this.load();
|
|
var content = this.read_text();
|
|
var bookDoc = domParser.parseFromString(content, "text/html")
|
|
.documentElement;
|
|
let lines = Array.from(
|
|
bookDoc.querySelectorAll("p,b,font,h3,h2,h1")
|
|
);
|
|
let parseContent = [];
|
|
for (let i = 0, len = lines.length; i < len - 1; i++) {
|
|
lines[i].innerText &&
|
|
lines[i].innerText !== parseContent[parseContent.length - 1] &&
|
|
parseContent.push(lines[i].innerText);
|
|
let imgDoms = lines[i].getElementsByTagName("img");
|
|
if (imgDoms.length > 0) {
|
|
for (let i = 0; i < imgDoms.length; i++) {
|
|
parseContent.push("#image");
|
|
}
|
|
}
|
|
}
|
|
const handleImage = async () => {
|
|
var imgDoms = bookDoc.getElementsByTagName("img");
|
|
parseContent.push("~image");
|
|
for (let i = 0; i < imgDoms.length; i++) {
|
|
const src = await this.render_image(imgDoms, i);
|
|
parseContent.push(
|
|
src + " " + imgDoms[i].width + " " + imgDoms[i].height
|
|
);
|
|
}
|
|
if (imgDoms.length > 200 || !isElectron) {
|
|
resolve(bookDoc);
|
|
} else {
|
|
resolve(parseContent.join("\n \n"));
|
|
}
|
|
};
|
|
handleImage();
|
|
});
|
|
}
|
|
render_image = (imgDoms, i) => {
|
|
return new Promise((resolve, reject) => {
|
|
var imgDom = imgDoms[i];
|
|
var idx = +imgDom.getAttribute("recindex");
|
|
var blob = this.read_image(idx - 1);
|
|
var imgReader = new FileReader();
|
|
imgReader.onload = (e) => {
|
|
imgDom.src = e.target?.result;
|
|
resolve(e.target?.result);
|
|
};
|
|
imgReader.onerror = function (err) {
|
|
reject(err);
|
|
};
|
|
imgReader.readAsDataURL(blob);
|
|
});
|
|
};
|
|
}
|
|
|
|
export default MobiFile;
|