mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-10 19:31:39 +00:00
443 lines
11 KiB
JavaScript
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
|