audiobookshelf/client/assets/ebooks/mobi.js
2024-10-16 20:41:17 -04:00

443 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