From 55c0027d111664ac4de8254f3b75df7b2a38a096 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Sat, 2 Aug 2025 07:19:23 +0800 Subject: [PATCH] Speed up text sectioning for large MOBI books (#92) --- mobi.js | 46 ++++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/mobi.js b/mobi.js index 939d6dd..4536be8 100644 --- a/mobi.js +++ b/mobi.js @@ -658,6 +658,15 @@ const getIndent = el => { return x } +function rawBytesToString(uint8Array) { + const chunkSize = 0x8000 + let result = '' + for (let i = 0; i < uint8Array.length; i += chunkSize) { + result += String.fromCharCode.apply(null, uint8Array.subarray(i, i + chunkSize)) + } + return result +} + class MOBI6 { parser = new DOMParser() serializer = new XMLSerializer() @@ -671,32 +680,37 @@ class MOBI6 { this.mobi = mobi } async init() { + const recordBuffers = [] + for (let i = 0; i < this.mobi.headers.palmdoc.numTextRecords; i++) { + const buf = await this.mobi.loadText(i) + recordBuffers.push(buf) + } + const totalLength = recordBuffers.reduce((sum, buf) => sum + buf.byteLength, 0) // load all text records in an array - let array = new Uint8Array() - for (let i = 0; i < this.mobi.headers.palmdoc.numTextRecords; i++) - array = concatTypedArray(array, await this.mobi.loadText(i)) - + const array = new Uint8Array(totalLength) + recordBuffers.reduce((offset, buf) => { + array.set(new Uint8Array(buf), offset) + return offset + buf.byteLength + }, 0) // convert to string so we can use regex // note that `filepos` are byte offsets // so it needs to preserve each byte as a separate character // (see https://stackoverflow.com/q/50198017) - const str = Array.from(new Uint8Array(array), - c => String.fromCharCode(c)).join('') + const str = rawBytesToString(array) // split content into sections at each `` this.#sections = [0] .concat(Array.from(str.matchAll(mbpPagebreakRegex), m => m.index)) - .map((x, i, a) => str.slice(x, a[i + 1])) - // recover the original raw bytes - .map(str => Uint8Array.from(str, x => x.charCodeAt(0))) - .map(raw => ({ book: this, raw })) + .map((start, i, a) => { + const end = a[i + 1] ?? array.length + return { book: this, raw: array.subarray(start, end) } + }) // get start and end filepos for each section - .reduce((arr, x) => { - const last = arr[arr.length - 1] - x.start = last?.end ?? 0 - x.end = x.start + x.raw.byteLength - return arr.concat(x) - }, []) + .map((section, i, arr) => { + section.start = arr[i - 1]?.end ?? 0 + section.end = section.start + section.raw.byteLength + return section + }) this.sections = this.#sections.map((section, index) => ({ id: index,