/* 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