mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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;
 |