优化
This commit is contained in:
@@ -24,7 +24,7 @@ object AppPattern {
|
||||
val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
|
||||
|
||||
//本地书籍支持类型
|
||||
val bookFileRegex = Regex(".*\\.(txt|epub|umd|pdf)", RegexOption.IGNORE_CASE)
|
||||
val bookFileRegex = Regex(".*\\.(txt|epub|umd|pdf|mobi|azw3|azw)", RegexOption.IGNORE_CASE)
|
||||
//压缩文件支持类型
|
||||
val archiveFileRegex = Regex(".*\\.(zip|rar|7z)$", RegexOption.IGNORE_CASE)
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@ val Book.isUmd: Boolean
|
||||
val Book.isPdf: Boolean
|
||||
get() = isLocal && originName.endsWith(".pdf", true)
|
||||
|
||||
val Book.isMobi: Boolean
|
||||
get() = isLocal && (originName.endsWith(".mobi", true) ||
|
||||
originName.endsWith(".azw3", true) ||
|
||||
originName.endsWith(".azw", true))
|
||||
|
||||
val Book.isOnLineTxt: Boolean
|
||||
get() = !isLocal && isType(BookType.text)
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package io.legado.app.lib.mobi
|
||||
|
||||
import io.legado.app.lib.mobi.entities.KF6Section
|
||||
import io.legado.app.lib.mobi.entities.MobiEntryHeaders
|
||||
import io.legado.app.lib.mobi.entities.NCX
|
||||
import io.legado.app.lib.mobi.entities.TOC
|
||||
import java.nio.CharBuffer
|
||||
|
||||
|
||||
/**
|
||||
* Kindle Format 6 Book
|
||||
*/
|
||||
class KF6Book(
|
||||
pdbFile: PDBFile,
|
||||
headers: MobiEntryHeaders,
|
||||
kf8BoundaryOffset: Int,
|
||||
resourceStart: Int
|
||||
) : MobiBook(pdbFile, headers, kf8BoundaryOffset, resourceStart) {
|
||||
|
||||
lateinit var sections: List<KF6Section>
|
||||
lateinit var sectionIdMap: LinkedHashMap<Int, ArrayList<TOC>>
|
||||
|
||||
init {
|
||||
processSections()
|
||||
processNCX()
|
||||
processSectionsMap()
|
||||
}
|
||||
|
||||
fun getResourceByHref(href: String): ByteArray? {
|
||||
val recindex = href.substringAfter("recindex:").toIntOrNull() ?: return null
|
||||
return getResource(recindex - 1).array()
|
||||
}
|
||||
|
||||
fun getSectionText(section: KF6Section): String {
|
||||
val inputStream = getTextRecordInputStream()
|
||||
val byteArray = ByteArray(section.length)
|
||||
inputStream.skip(section.start.toLong())
|
||||
inputStream.read(byteArray)
|
||||
return String(byteArray, charset)
|
||||
}
|
||||
|
||||
fun getSectionByHref(href: String): KF6Section? {
|
||||
val index = getIndexByHref(href)
|
||||
return sections.getOrNull(index)
|
||||
}
|
||||
|
||||
private fun processSectionsMap() {
|
||||
sectionIdMap = linkedMapOf()
|
||||
if (toc == null) {
|
||||
return
|
||||
}
|
||||
fun fmap(item: TOC) {
|
||||
val index = getIndexByHref(item.href)
|
||||
if (index == -1) return
|
||||
val array = sectionIdMap.getOrPut(index) { arrayListOf() }
|
||||
array.add(item)
|
||||
item.subitems?.forEach(::fmap)
|
||||
}
|
||||
toc!!.forEach(::fmap)
|
||||
}
|
||||
|
||||
private fun getIndexByHref(href: String): Int {
|
||||
val filepos = href.substringAfter("filepos:").toIntOrNull() ?: return -1
|
||||
return sections.indexOfFirst { it.end > filepos }
|
||||
}
|
||||
|
||||
private fun processNCX() {
|
||||
val ncx = getNCX() ?: return
|
||||
fun fmap(item: NCX): TOC {
|
||||
val filepos = item.offset!!
|
||||
val href = "filepos:${filepos.toString().padStart(10, '0')}"
|
||||
return TOC(item.label, href, item.children?.map(::fmap))
|
||||
}
|
||||
toc = ncx.map(::fmap)
|
||||
}
|
||||
|
||||
private fun processSections() {
|
||||
val sections = arrayListOf<KF6Section>()
|
||||
val pattern = mbpPagebreakRegex.toPattern()
|
||||
val inputStream = getTextRecordInputStream()
|
||||
val available = inputStream.available()
|
||||
val reader = inputStream.reader(Charsets.ISO_8859_1)
|
||||
var buffer = CharBuffer.allocate(4096)
|
||||
reader.read(buffer)
|
||||
buffer.flip()
|
||||
val matcher = pattern.matcher(buffer)
|
||||
var droppedOffset = 0
|
||||
var nextStart = 0
|
||||
var position = 0
|
||||
while (true) {
|
||||
if (!matcher.find()) {
|
||||
buffer.position(position)
|
||||
if (buffer.limit() == buffer.capacity()) {
|
||||
if (position > 0) {
|
||||
buffer.compact()
|
||||
} else {
|
||||
val newBuf = CharBuffer.allocate(buffer.capacity() * 2)
|
||||
newBuf.put(buffer)
|
||||
buffer = newBuf
|
||||
}
|
||||
droppedOffset += position
|
||||
position = 0
|
||||
}
|
||||
if (reader.read(buffer) == -1) {
|
||||
break
|
||||
}
|
||||
buffer.flip()
|
||||
matcher.reset(buffer)
|
||||
} else {
|
||||
val last = sections.lastOrNull()
|
||||
val index = sections.size
|
||||
val start = nextStart
|
||||
val end = matcher.start() + droppedOffset
|
||||
nextStart = matcher.end() + droppedOffset
|
||||
position = matcher.end()
|
||||
val length = end - start
|
||||
val href = "filepos:${start.toString().padStart(10, '0')}"
|
||||
val section = KF6Section(index, start, end, length, href)
|
||||
last?.next = section
|
||||
sections.add(section)
|
||||
}
|
||||
}
|
||||
if (nextStart > 0) {
|
||||
val last = sections.lastOrNull()
|
||||
val index = sections.size
|
||||
val start = nextStart
|
||||
val length = available - start
|
||||
val href = "filepos:${start.toString().padStart(10, '0')}"
|
||||
val section = KF6Section(index, start, available, length, href)
|
||||
last?.next = section
|
||||
sections.add(section)
|
||||
}
|
||||
this.sections = sections
|
||||
}
|
||||
|
||||
companion object {
|
||||
val mbpPagebreakRegex = "(?i)<\\s*(?:mbp:)?pagebreak[^>]*>".toRegex()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package io.legado.app.lib.mobi
|
||||
|
||||
import io.legado.app.lib.mobi.entities.FdstHeader
|
||||
import io.legado.app.lib.mobi.entities.Fragment
|
||||
import io.legado.app.lib.mobi.entities.KF8Pos
|
||||
import io.legado.app.lib.mobi.entities.KF8Resource
|
||||
import io.legado.app.lib.mobi.entities.KF8Section
|
||||
import io.legado.app.lib.mobi.entities.MobiEntryHeaders
|
||||
import io.legado.app.lib.mobi.entities.NCX
|
||||
import io.legado.app.lib.mobi.entities.Skeleton
|
||||
import io.legado.app.lib.mobi.entities.TOC
|
||||
import io.legado.app.lib.mobi.utils.readString
|
||||
import io.legado.app.lib.mobi.utils.readUInt32
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Kindle Format 8 Book
|
||||
*/
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class KF8Book(
|
||||
pdbFile: PDBFile,
|
||||
headers: MobiEntryHeaders,
|
||||
kf8BoundaryOffset: Int,
|
||||
resourceStart: Int
|
||||
) : MobiBook(pdbFile, headers, kf8BoundaryOffset, resourceStart) {
|
||||
|
||||
private var fdstTableStarts: IntArray? = null
|
||||
private var fdstTableEnds: IntArray? = null
|
||||
private lateinit var skelTable: List<Skeleton>
|
||||
private lateinit var fragTable: List<Fragment>
|
||||
private var kf8 = headers.kf8!!
|
||||
lateinit var sections: List<KF8Section>
|
||||
lateinit var sectionIdMap: LinkedHashMap<Int, ArrayList<TOC>>
|
||||
|
||||
init {
|
||||
readFdstTable()
|
||||
readSkelTable()
|
||||
readFragTable()
|
||||
processSections()
|
||||
processNCX()
|
||||
processSectionsMap()
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立 section id -> toc 的 map
|
||||
*/
|
||||
private fun processSectionsMap() {
|
||||
sectionIdMap = linkedMapOf()
|
||||
if (toc == null) {
|
||||
return
|
||||
}
|
||||
fun fmap(item: TOC) {
|
||||
val index = getIndexByHref(item.href)
|
||||
if (index == -1) return
|
||||
val array = sectionIdMap.getOrPut(index) { arrayListOf() }
|
||||
array.add(item)
|
||||
item.subitems?.forEach(::fmap)
|
||||
}
|
||||
toc!!.forEach(::fmap)
|
||||
}
|
||||
|
||||
fun parsePosURI(href: String): KF8Pos? {
|
||||
val match = kindlePosRegex.find(href) ?: return null
|
||||
val fid = match.groupValues[1].toInt(32)
|
||||
val off = match.groupValues[2].toInt(32)
|
||||
return KF8Pos(fid, off)
|
||||
}
|
||||
|
||||
private fun parseResourceURI(href: String): KF8Resource? {
|
||||
val match = kindleResourceRegex.find(href) ?: return null
|
||||
val resourceType = match.groupValues[1]
|
||||
val id = match.groupValues[2].toInt(32)
|
||||
val type = match.groupValues[3]
|
||||
return KF8Resource(resourceType, id, type)
|
||||
}
|
||||
|
||||
fun getResourceByHref(href: String): ByteArray? {
|
||||
val resource = parseResourceURI(href) ?: return null
|
||||
if (resource.resourceType == "flow") return null
|
||||
return getResource(resource.id - 1).array()
|
||||
}
|
||||
|
||||
fun getSectionByHref(href: String): KF8Section? {
|
||||
val index = getIndexByHref(href)
|
||||
return sections.getOrNull(index)
|
||||
}
|
||||
|
||||
private fun getIndexByHref(href: String): Int {
|
||||
val pos = parsePosURI(href) ?: return -1
|
||||
return getIndexByFID(pos.fid)
|
||||
}
|
||||
|
||||
private fun getIndexByFID(fid: Int): Int {
|
||||
return sections.indexOfFirst {
|
||||
it.frags.any { frag ->
|
||||
frag.index == fid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTextByHref(href: String, nextHref: String): String {
|
||||
val pos = parsePosURI(href) ?: return ""
|
||||
val nextPos = parsePosURI(nextHref) ?: return ""
|
||||
val index = getIndexByFID(pos.fid)
|
||||
val nextIndex = getIndexByFID(nextPos.fid)
|
||||
val startFid = pos.fid
|
||||
val endFid = if (index == nextIndex) nextPos.fid else Int.MAX_VALUE
|
||||
val section = sections[index]
|
||||
val skel = section.skeleton
|
||||
val droppedFrags = section.frags.filter { it.index < startFid }
|
||||
var droppedFragsLength = droppedFrags.sumOf { it.length }
|
||||
val frags = section.frags.filter { it.index in startFid..endFid }
|
||||
val length = skel.length + frags.sumOf { it.length }
|
||||
val raw = getRaw(skel.offset, section.length)
|
||||
val lastFragDroppedLength =
|
||||
if (index == nextIndex) frags.last().length - nextPos.offset else 0
|
||||
val skeleton = ByteArray(length - pos.offset - lastFragDroppedLength)
|
||||
var leftBytes = skeleton.size
|
||||
raw.copyInto(skeleton, 0, 0, skel.length)
|
||||
leftBytes -= skel.length
|
||||
for ((i, frag) in frags.withIndex()) {
|
||||
val isFirstFrag = i == 0
|
||||
val isLastFrag = i == frags.lastIndex
|
||||
val insertOffset = frag.insertOffset - skel.offset - droppedFragsLength
|
||||
val offset = skel.length + frag.offset
|
||||
skeleton.copyInto(
|
||||
skeleton,
|
||||
insertOffset + frag.length -
|
||||
(if (isFirstFrag) pos.offset else 0) -
|
||||
(if (isLastFrag) lastFragDroppedLength else 0),
|
||||
insertOffset,
|
||||
skeleton.size - leftBytes
|
||||
)
|
||||
raw.copyInto(
|
||||
skeleton,
|
||||
insertOffset,
|
||||
offset + if (isFirstFrag) pos.offset else 0,
|
||||
offset + frag.length - if (isLastFrag) lastFragDroppedLength else 0
|
||||
)
|
||||
leftBytes -= frag.length -
|
||||
(if (isFirstFrag) pos.offset else 0) -
|
||||
(if (isLastFrag && index == nextIndex) nextPos.offset else 0)
|
||||
if (isFirstFrag) {
|
||||
droppedFragsLength += pos.offset
|
||||
}
|
||||
}
|
||||
return String(skeleton, charset)
|
||||
}
|
||||
|
||||
fun getSectionText(section: KF8Section): String {
|
||||
val skel = section.skeleton
|
||||
val frags = section.frags
|
||||
val length = section.length
|
||||
//val raw = getRaw(skel.offset, skel.offset + length)
|
||||
val raw = getRaw(skel.offset, length)
|
||||
val skeleton = ByteArray(raw.size)
|
||||
var leftBytes = raw.size
|
||||
raw.copyInto(skeleton, 0, 0, skel.length)
|
||||
leftBytes -= skel.length
|
||||
for (frag in frags) {
|
||||
val insertOffset = frag.insertOffset - skel.offset
|
||||
val offset = skel.length + frag.offset
|
||||
skeleton.copyInto(
|
||||
skeleton,
|
||||
insertOffset + frag.length,
|
||||
insertOffset,
|
||||
skeleton.size - leftBytes
|
||||
)
|
||||
raw.copyInto(skeleton, insertOffset, offset, offset + frag.length)
|
||||
leftBytes -= frag.length
|
||||
}
|
||||
return String(skeleton, charset)
|
||||
}
|
||||
|
||||
private fun getRaw(offset: Int, len: Int): ByteArray {
|
||||
val inputStream = getTextRecordInputStream()
|
||||
val byteArray = ByteArray(len)
|
||||
inputStream.skip(offset.toLong())
|
||||
inputStream.read(byteArray)
|
||||
return byteArray
|
||||
}
|
||||
|
||||
private fun processNCX() {
|
||||
val ncx = getNCX() ?: return
|
||||
fun fmap(item: NCX): TOC {
|
||||
val (fid, off) = item.pos!!
|
||||
val href = makePosURI(fid, off)
|
||||
return TOC(item.label, href, item.children?.map(::fmap))
|
||||
}
|
||||
toc = ncx.map(::fmap)
|
||||
}
|
||||
|
||||
private fun makePosURI(fid: Int, off: Int): String {
|
||||
val encodedFid = fid.toString(32).uppercase(Locale.ROOT).padStart(4, '0')
|
||||
val encodedOff = off.toString(32).uppercase(Locale.ROOT).padStart(10, '0')
|
||||
return "kindle:pos:fid:$encodedFid:off:$encodedOff"
|
||||
}
|
||||
|
||||
private fun processSections() {
|
||||
sections = skelTable.fold(arrayListOf()) { arr, skel ->
|
||||
val last = arr.lastOrNull()
|
||||
val index = arr.size
|
||||
val fragStart = last?.fragEnd ?: 0
|
||||
val fragEnd = fragStart + skel.numFrag
|
||||
val frags = fragTable.slice(fragStart..<fragEnd)
|
||||
val length = skel.length + frags.sumOf { it.length }
|
||||
val totalLength = (last?.totalLength ?: 0) + length
|
||||
val href = frags.firstOrNull()?.let { makePosURI(it.index, 0) } ?: ""
|
||||
val section = KF8Section(index, skel, frags, fragEnd, length, totalLength, href)
|
||||
last?.next = section
|
||||
arr.add(section)
|
||||
arr
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFragTable() {
|
||||
val fragData = getIndexData(kf8.frag)
|
||||
fragTable = fragData.table.map { indexEntry ->
|
||||
val tagMap = indexEntry.tagMap
|
||||
Fragment(
|
||||
indexEntry.label.toInt(),
|
||||
fragData.cncx[tagMap[2].tagValues[0]],
|
||||
tagMap[4].tagValues[0],
|
||||
tagMap[6].tagValues[0],
|
||||
tagMap[6].tagValues[1]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readSkelTable() {
|
||||
skelTable = getIndexData(kf8.skel).table.mapIndexed { index, indexEntry ->
|
||||
val tagMap = indexEntry.tagMap
|
||||
Skeleton(
|
||||
index,
|
||||
indexEntry.label,
|
||||
tagMap[1].tagValues[0],
|
||||
tagMap[6].tagValues[0],
|
||||
tagMap[6].tagValues[1],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFdstTable() {
|
||||
try {
|
||||
val fdstBuffer = getRecord(kf8.fdst)
|
||||
val fdstHeader = readFdstHeader(fdstBuffer)
|
||||
fdstTableStarts = IntArray(fdstHeader.numEntries)
|
||||
fdstTableEnds = IntArray(fdstHeader.numEntries)
|
||||
fdstBuffer.position(12)
|
||||
for (i in 0..<fdstHeader.numEntries) {
|
||||
fdstTableStarts!![i] = fdstBuffer.readUInt32()
|
||||
fdstTableEnds!![i] = fdstBuffer.readUInt32()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun readFdstHeader(buffer: ByteBuffer): FdstHeader {
|
||||
val magic = buffer.readString(0, 4)
|
||||
if (magic != "FDST") error("Missing FDST record")
|
||||
val numEntries = buffer.readUInt32(8)
|
||||
return FdstHeader(magic, numEntries)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val kindlePosRegex = "kindle:pos:fid:(\\w+):off:(\\w+)".toRegex()
|
||||
val kindleResourceRegex =
|
||||
"kindle:(flow|embed):(\\w+)(?:\\?mime=(\\w+/[-+.\\w]+))?".toRegex()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
package io.legado.app.lib.mobi
|
||||
|
||||
import android.util.SparseArray
|
||||
import io.legado.app.lib.mobi.decompress.Decompressor
|
||||
import io.legado.app.lib.mobi.decompress.HuffcdicDecompressor
|
||||
import io.legado.app.lib.mobi.decompress.Lz77Decompressor
|
||||
import io.legado.app.lib.mobi.decompress.PlainDecompressor
|
||||
import io.legado.app.lib.mobi.entities.IndexData
|
||||
import io.legado.app.lib.mobi.entities.IndexEntry
|
||||
import io.legado.app.lib.mobi.entities.IndexTag
|
||||
import io.legado.app.lib.mobi.entities.IndxHeader
|
||||
import io.legado.app.lib.mobi.entities.MobiEntryHeaders
|
||||
import io.legado.app.lib.mobi.entities.MobiMetadata
|
||||
import io.legado.app.lib.mobi.entities.NCX
|
||||
import io.legado.app.lib.mobi.entities.Ptagx
|
||||
import io.legado.app.lib.mobi.entities.TOC
|
||||
import io.legado.app.lib.mobi.entities.TagxHeader
|
||||
import io.legado.app.lib.mobi.entities.TagxTag
|
||||
import io.legado.app.lib.mobi.utils.readString
|
||||
import io.legado.app.lib.mobi.utils.readUInt16Array
|
||||
import io.legado.app.lib.mobi.utils.readUInt32
|
||||
import io.legado.app.lib.mobi.utils.readUInt8
|
||||
import okhttp3.internal.and
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
abstract class MobiBook(
|
||||
private val pdbFile: PDBFile,
|
||||
val headers: MobiEntryHeaders,
|
||||
private val kf8BoundaryOffset: Int,
|
||||
private val resourceStart: Int,
|
||||
) {
|
||||
val mobi = headers.mobi
|
||||
val palmdoc = headers.palmdoc
|
||||
private val exth = headers.exth
|
||||
private val trailingFlags = mobi.trailingFlags
|
||||
|
||||
val charset: Charset = when (val charset = mobi.encoding) {
|
||||
65001 -> Charsets.UTF_8
|
||||
1252 -> Charset.forName("windows-1252")
|
||||
else -> error("unknown charset $charset")
|
||||
}
|
||||
|
||||
private val decompressor: Decompressor = when (val compression = palmdoc.compression) {
|
||||
1 -> PlainDecompressor()
|
||||
2 -> Lz77Decompressor(max(4096, palmdoc.recordSize))
|
||||
17480 -> HuffcdicDecompressor(this, mobi)
|
||||
else -> error("unknown compression $charset")
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val metadata: MobiMetadata by lazy {
|
||||
MobiMetadata(
|
||||
mobi.uid.toString(),
|
||||
exth["title"] as? String ?: mobi.title,
|
||||
exth["creator"] as? List<String> ?: emptyList(),
|
||||
exth["publisher"] as? String ?: "",
|
||||
exth["language"] as? String ?: mobi.languege,
|
||||
exth["date"] as? String ?: "",
|
||||
exth["description"] as? String ?: "",
|
||||
exth["subject"] as? List<String> ?: emptyList(),
|
||||
exth["rights"] as? String ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
var toc: List<TOC>? = null
|
||||
|
||||
private var textRecordOffsets = arrayListOf<Int>()
|
||||
|
||||
init {
|
||||
buildTextRecordOffsets()
|
||||
}
|
||||
|
||||
private fun buildTextRecordOffsets() {
|
||||
var offset = 0
|
||||
for (i in 0..<palmdoc.numTextRecords) {
|
||||
offset += getTextRecord(i).size
|
||||
textRecordOffsets.add(offset)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRecord(index: Int): ByteBuffer {
|
||||
return pdbFile.getRecordData(kf8BoundaryOffset + index)
|
||||
}
|
||||
|
||||
fun getTextRecord(index: Int): ByteArray {
|
||||
if (index < 0 && index >= palmdoc.numTextRecords) {
|
||||
throw IndexOutOfBoundsException("Text record index out of bounds")
|
||||
}
|
||||
var content = getRecord(index + 1).array()
|
||||
content = removeTrailingEntries(content)
|
||||
return decompressor.decompress(content)
|
||||
}
|
||||
|
||||
fun getTextRecordInputStream(): InputStream {
|
||||
return object : InputStream() {
|
||||
|
||||
private var index = -1
|
||||
private var bis: ByteArrayInputStream = emptyByteArrayInputStream
|
||||
private var available = textRecordOffsets.last()
|
||||
private var pos = 0
|
||||
|
||||
override fun read(): Int {
|
||||
if (index >= palmdoc.numTextRecords) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (bis.available() == 0) {
|
||||
if (++index >= palmdoc.numTextRecords) {
|
||||
return -1
|
||||
}
|
||||
bis = getTextRecord(index).inputStream()
|
||||
}
|
||||
|
||||
val b = bis.read()
|
||||
|
||||
available--
|
||||
pos++
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
override fun skip(n: Long): Long {
|
||||
if (n == 0L) return 0L
|
||||
|
||||
val n1 = min(available, n.toInt())
|
||||
|
||||
if (n1 < bis.available()) {
|
||||
bis.skip(n1.toLong())
|
||||
|
||||
available -= n1
|
||||
pos += n1
|
||||
|
||||
return n1.toLong()
|
||||
}
|
||||
|
||||
val bIndex = textRecordOffsets.binarySearch(pos + n1)
|
||||
index = abs(bIndex + 1)
|
||||
|
||||
bis = getTextRecord(index).inputStream()
|
||||
|
||||
val offset = textRecordOffsets.getOrNull(index - 1) ?: 0
|
||||
bis.skip((pos + n1 - offset).toLong())
|
||||
|
||||
available -= n1
|
||||
pos += n1
|
||||
|
||||
return n1.toLong()
|
||||
}
|
||||
|
||||
override fun available(): Int {
|
||||
return available
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeTrailingEntries(byteArray: ByteArray): ByteArray {
|
||||
if (trailingFlags == 0) return byteArray
|
||||
val multibyte = trailingFlags and 1 != 0
|
||||
val numTrailingEntries = (trailingFlags shr 1).countOneBits()
|
||||
val lastIndex = byteArray.lastIndex
|
||||
var extraSize = 0
|
||||
for (i in 0..<numTrailingEntries) {
|
||||
var value = 0
|
||||
for (j in max(0, lastIndex - 4 - extraSize)..max(0, lastIndex - extraSize)) {
|
||||
val byte = byteArray[j]
|
||||
if (byte and 0b1000_0000 != 0) {
|
||||
value = 0
|
||||
}
|
||||
value = (value shl 7) or (byte and 0b111_1111)
|
||||
}
|
||||
extraSize += value
|
||||
}
|
||||
if (multibyte) {
|
||||
val byte = byteArray[byteArray.lastIndex - extraSize]
|
||||
extraSize += (byte and 0b11) + 1
|
||||
}
|
||||
return byteArray.copyOfRange(0, byteArray.size - extraSize)
|
||||
}
|
||||
|
||||
fun getResource(index: Int): ByteBuffer {
|
||||
return pdbFile.getRecordData(resourceStart + index)
|
||||
}
|
||||
|
||||
fun getNCX(): List<NCX>? {
|
||||
val indxIndex = mobi.indx
|
||||
if (indxIndex == -1) {
|
||||
return null
|
||||
}
|
||||
val indexData = getIndexData(indxIndex)
|
||||
val items = indexData.table.mapIndexed { index, indexEntry ->
|
||||
val tagMap = indexEntry.tagMap
|
||||
NCX(
|
||||
index,
|
||||
tagMap[1]?.tagValues?.getOrNull(0),
|
||||
tagMap[2]?.tagValues?.getOrNull(0),
|
||||
indexData.cncx[tagMap[3].tagValues[0]],
|
||||
tagMap[4]?.tagValues?.getOrNull(0),
|
||||
tagMap[6]?.tagValues,
|
||||
tagMap[21]?.tagValues?.getOrNull(0),
|
||||
tagMap[22]?.tagValues?.getOrNull(0),
|
||||
tagMap[23]?.tagValues?.getOrNull(0),
|
||||
)
|
||||
}
|
||||
|
||||
val parentItemMap = hashMapOf<Int, ArrayList<NCX>>()
|
||||
|
||||
items.forEach {
|
||||
val parent = it.parent ?: return@forEach
|
||||
val array = parentItemMap.getOrPut(parent) { arrayListOf() }
|
||||
array.add(it)
|
||||
}
|
||||
|
||||
fun getChildren(item: NCX): NCX {
|
||||
if (item.firstChild == null) return item
|
||||
item.children = parentItemMap[item.index]?.map(::getChildren)
|
||||
return item
|
||||
}
|
||||
|
||||
return items.filter { it.headingLevel == 0 }.map(::getChildren)
|
||||
}
|
||||
|
||||
fun getCover(): ByteArray? {
|
||||
val coverOffset = exth["coverOffset"] as? Int
|
||||
val thumbnailOffset = exth["thumbnailOffset"] as? Int
|
||||
|
||||
if (coverOffset != null && coverOffset != -1) {
|
||||
return getResource(coverOffset).array()
|
||||
}
|
||||
|
||||
if (thumbnailOffset != null && thumbnailOffset != -1) {
|
||||
return getResource(thumbnailOffset).array()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun getIndexData(indxIndex: Int): IndexData {
|
||||
val indxRecord = getRecord(indxIndex)
|
||||
val indx = readIndxHeader(indxRecord)
|
||||
indxRecord.position(indx.length)
|
||||
val tagxBuffer = indxRecord.slice()
|
||||
val tagx = readTagxHeader(tagxBuffer)
|
||||
val tagTable = readTagxTags(tagx, tagxBuffer)
|
||||
val cncx = readCncx(indxIndex, indx)
|
||||
|
||||
val table = arrayListOf<IndexEntry>()
|
||||
|
||||
for (i in 0..<indx.numRecords) {
|
||||
val indxBuffer = getRecord(indxIndex + 1 + i)
|
||||
val indxHeader = readIndxHeader(indxBuffer)
|
||||
val idxt = readIdxt(indxBuffer, indxHeader)
|
||||
for (j in 0..<indxHeader.numRecords) {
|
||||
val idxtOffset = idxt[j]
|
||||
val entry = readIndexEntry(indxBuffer, tagx, tagTable, idxtOffset)
|
||||
table.add(entry)
|
||||
}
|
||||
}
|
||||
|
||||
return IndexData(table, cncx)
|
||||
}
|
||||
|
||||
private fun readIndexEntry(
|
||||
indxBuffer: ByteBuffer,
|
||||
tagx: TagxHeader,
|
||||
tagTable: List<TagxTag>,
|
||||
idxtOffset: Int
|
||||
): IndexEntry {
|
||||
val array = indxBuffer.array()
|
||||
|
||||
val len = indxBuffer.readUInt8(idxtOffset)
|
||||
val label = indxBuffer.readString(idxtOffset + 1, len)
|
||||
|
||||
val ptagxs = arrayListOf<Ptagx>()
|
||||
val startPos = idxtOffset + 1 + len
|
||||
var controlByteIndex = 0
|
||||
var pos = startPos + tagx.numControlBytes
|
||||
|
||||
for (tag in tagTable) {
|
||||
if (tag.controlByte == 1) {
|
||||
controlByteIndex++
|
||||
continue
|
||||
}
|
||||
val offset = startPos + controlByteIndex
|
||||
var value = indxBuffer.readUInt8(offset) and tag.bitmask
|
||||
if (value == tag.bitmask) {
|
||||
if (tag.bitmask.countOneBits() > 1) {
|
||||
var v = 0
|
||||
for (a in pos..<min(pos + 4, array.size)) {
|
||||
val byte = array[a]
|
||||
v = (v shl 7) or (byte and 0b111_1111)
|
||||
pos++
|
||||
if (byte and 0b1000_0000 != 0) break
|
||||
}
|
||||
ptagxs.add(Ptagx(tag.tag, tag.numValues, null, v))
|
||||
} else {
|
||||
ptagxs.add(Ptagx(tag.tag, tag.numValues, 1, null))
|
||||
}
|
||||
} else {
|
||||
var mask = tag.bitmask
|
||||
while ((mask and 1) == 0) {
|
||||
mask = mask shr 1
|
||||
value = value shr 1
|
||||
}
|
||||
ptagxs.add(Ptagx(tag.tag, tag.numValues, value, null))
|
||||
}
|
||||
}
|
||||
|
||||
val tags = arrayListOf<IndexTag>()
|
||||
val tagMap = SparseArray<IndexTag>()
|
||||
|
||||
for (ptagx in ptagxs) {
|
||||
val values = arrayListOf<Int>()
|
||||
if (ptagx.valueCount != null) {
|
||||
repeat(ptagx.valueCount * ptagx.tagValueCount) {
|
||||
var v = 0
|
||||
for (a in pos..<min(pos + 4, array.size)) {
|
||||
val byte = array[a]
|
||||
v = (v shl 7) or (byte and 0b111_1111)
|
||||
pos++
|
||||
if (byte and 0b1000_0000 != 0) break
|
||||
}
|
||||
values.add(v)
|
||||
}
|
||||
} else {
|
||||
var count = 0
|
||||
while (count < ptagx.valueBytes!!) {
|
||||
var v = 0
|
||||
for (a in pos..<min(pos + 4, array.size)) {
|
||||
val byte = array[a]
|
||||
v = (v shl 7) or (byte and 0b111_1111)
|
||||
pos++
|
||||
count++
|
||||
if (byte and 0b1000_0000 != 0) break
|
||||
}
|
||||
values.add(v)
|
||||
}
|
||||
}
|
||||
val tag = IndexTag(ptagx.tag, values)
|
||||
tags.add(tag)
|
||||
tagMap[tag.tagId] = tag
|
||||
}
|
||||
|
||||
return IndexEntry(label, tags, tagMap)
|
||||
}
|
||||
|
||||
private fun readIdxt(buffer: ByteBuffer, indxHeader: IndxHeader): IntArray {
|
||||
return buffer.readUInt16Array(indxHeader.idxt + 4, indxHeader.numRecords)
|
||||
}
|
||||
|
||||
private fun readTagxTags(tagx: TagxHeader, tagxBuffer: ByteBuffer): List<TagxTag> {
|
||||
val numTags = (tagx.length - 12) / 4
|
||||
val tags = arrayListOf<TagxTag>()
|
||||
tagxBuffer.position(12)
|
||||
for (i in 0..<numTags) {
|
||||
val tag = tagxBuffer.readUInt8()
|
||||
val numValues = tagxBuffer.readUInt8()
|
||||
val bitmask = tagxBuffer.readUInt8()
|
||||
val controlByte = tagxBuffer.readUInt8()
|
||||
tags.add(TagxTag(tag, numValues, bitmask, controlByte))
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
private fun readCncx(indxIndex: Int, indx: IndxHeader): SparseArray<String> {
|
||||
val cncx = SparseArray<String>()
|
||||
var cncxRecordOffset = 0
|
||||
for (i in 0..<indx.numCncx) {
|
||||
val record = getRecord(indxIndex + indx.numRecords + i + 1)
|
||||
val array = record.array()
|
||||
var pos = 0
|
||||
while (pos < array.size) {
|
||||
val index = pos
|
||||
var value = 0
|
||||
var length = 0
|
||||
for (a in pos..<min(pos + 4, array.size)) {
|
||||
val byte = array[a]
|
||||
value = (value shl 7) or (byte and 0b111_1111)
|
||||
length++
|
||||
if (byte and 0b1000_0000 != 0) break
|
||||
}
|
||||
pos += length
|
||||
val result = record.readString(pos, value, charset)
|
||||
pos += value
|
||||
cncx[cncxRecordOffset + index] = result
|
||||
}
|
||||
cncxRecordOffset += 0x10000
|
||||
}
|
||||
|
||||
return cncx
|
||||
}
|
||||
|
||||
private fun readIndxHeader(indx: ByteBuffer): IndxHeader {
|
||||
val magic = indx.readString(0, 4)
|
||||
if (magic != "INDX") {
|
||||
error("Invalid INDX record")
|
||||
}
|
||||
val length = indx.readUInt32(4)
|
||||
val type = indx.readUInt32(8)
|
||||
val idxt = indx.readUInt32(20)
|
||||
val numRecords = indx.readUInt32(24)
|
||||
val encoding = indx.readUInt32(28)
|
||||
val language = indx.readUInt32(32)
|
||||
val total = indx.readUInt32(36)
|
||||
val ordt = indx.readUInt32(40)
|
||||
val ligt = indx.readUInt32(44)
|
||||
val numLigt = indx.readUInt32(48)
|
||||
val numCncx = indx.readUInt32(52)
|
||||
|
||||
return IndxHeader(
|
||||
magic, length, type, idxt, numRecords, encoding, language, total, ordt,
|
||||
ligt, numLigt, numCncx
|
||||
)
|
||||
}
|
||||
|
||||
private fun readTagxHeader(buffer: ByteBuffer): TagxHeader {
|
||||
val magic = buffer.readString(0, 4)
|
||||
if (magic != "TAGX") {
|
||||
error("Invalid INDX record")
|
||||
}
|
||||
val length = buffer.readUInt32(4)
|
||||
val numControlBytes = buffer.readUInt32(8)
|
||||
|
||||
return TagxHeader(magic, length, numControlBytes)
|
||||
}
|
||||
|
||||
fun close() {
|
||||
pdbFile.close()
|
||||
}
|
||||
|
||||
protected fun finalize() {
|
||||
close()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val emptyByteArrayInputStream = ByteArrayInputStream(ByteArray(0))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package io.legado.app.lib.mobi
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import io.legado.app.lib.mobi.entities.ExthRecordType
|
||||
import io.legado.app.lib.mobi.entities.KF8Header
|
||||
import io.legado.app.lib.mobi.entities.MobiEntryHeaders
|
||||
import io.legado.app.lib.mobi.entities.MobiHeader
|
||||
import io.legado.app.lib.mobi.entities.PalmDocHeader
|
||||
import io.legado.app.lib.mobi.utils.readString
|
||||
import io.legado.app.lib.mobi.utils.readUInt16
|
||||
import io.legado.app.lib.mobi.utils.readUInt32
|
||||
import io.legado.app.lib.mobi.utils.readUInt8
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class MobiReader {
|
||||
|
||||
fun readMobi(pfd: ParcelFileDescriptor): MobiBook {
|
||||
val pdbFile = PDBFile(pfd)
|
||||
val record0 = pdbFile.getRecordData(0)
|
||||
|
||||
var mobiEntryHeaders = readMobiEntryHeaders(record0)
|
||||
val mobi = mobiEntryHeaders.mobi
|
||||
val exth = mobiEntryHeaders.exth
|
||||
val resourceStart = mobi.resourceStart
|
||||
|
||||
var isKF8 = mobi.version >= 8
|
||||
|
||||
var kf8BoundaryOffset = 0
|
||||
|
||||
if (!isKF8) {
|
||||
val boundary = exth["boundary"] as? Int
|
||||
if (boundary != null && boundary != -1) {
|
||||
try {
|
||||
val buffer = pdbFile.getRecordData(boundary)
|
||||
mobiEntryHeaders = readMobiEntryHeaders(buffer)
|
||||
kf8BoundaryOffset = boundary
|
||||
isKF8 = true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (isKF8) {
|
||||
KF8Book(pdbFile, mobiEntryHeaders, kf8BoundaryOffset, resourceStart)
|
||||
} else {
|
||||
KF6Book(pdbFile, mobiEntryHeaders, kf8BoundaryOffset, resourceStart)
|
||||
}
|
||||
}
|
||||
|
||||
private fun readMobiEntryHeaders(buffer: ByteBuffer): MobiEntryHeaders {
|
||||
val palmDocHeader = readPalmDocHeader(buffer)
|
||||
val mobiHeader = readMobiHeader(buffer)
|
||||
val exth = if (mobiHeader.exthFlag and 0b100_0000 != 0) {
|
||||
buffer.position(mobiHeader.length + 16)
|
||||
readExth(buffer.slice())
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
val kF8Header = if (mobiHeader.version >= 8) {
|
||||
readKF8Header(buffer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return MobiEntryHeaders(palmDocHeader, mobiHeader, exth, kF8Header)
|
||||
}
|
||||
|
||||
private fun readExth(buffer: ByteBuffer): Map<String, Any> {
|
||||
val magic = buffer.readString(0, 4)
|
||||
check(magic == "EXTH") { "Invalid EXTH header" }
|
||||
val count = buffer.readUInt32(8)
|
||||
var offset = 12
|
||||
val map = HashMap<String, Any>()
|
||||
for (i in 0..<count) {
|
||||
val type = buffer.readUInt32(offset)
|
||||
val length = buffer.readUInt32(offset + 4)
|
||||
if (type in exthRecordTypeMap) {
|
||||
val exthRecordType = exthRecordTypeMap[type]!!
|
||||
val name = exthRecordType.name
|
||||
val data: Any = if (exthRecordType.type == "uint") {
|
||||
buffer.readUInt32(offset + 8)
|
||||
} else {
|
||||
buffer.readString(offset + 8, length - 8)
|
||||
}
|
||||
if (exthRecordType.many) {
|
||||
if (!map.contains(name)) {
|
||||
map[name] = arrayListOf<String>()
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val array = map[name] as ArrayList<String>
|
||||
array.add(data as String)
|
||||
} else {
|
||||
map[name] = data
|
||||
}
|
||||
}
|
||||
offset += length
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun readPalmDocHeader(content: ByteBuffer): PalmDocHeader {
|
||||
|
||||
val compression = content.readUInt16(0)
|
||||
val numTextRecords = content.readUInt16(8)
|
||||
val recordSize = content.readUInt16(10)
|
||||
val encryption = content.readUInt16(12)
|
||||
|
||||
return PalmDocHeader(compression, numTextRecords, recordSize, encryption)
|
||||
}
|
||||
|
||||
private fun readMobiHeader(content: ByteBuffer): MobiHeader {
|
||||
|
||||
val identifier = content.readString(16, 4)
|
||||
|
||||
check(identifier == "MOBI") { "Missing MOBI header" }
|
||||
|
||||
val length = content.readUInt32(20)
|
||||
val type = content.readUInt32(24)
|
||||
val encoding = content.readUInt32(28)
|
||||
val uid = content.readUInt32(32)
|
||||
val version = content.readUInt32(36)
|
||||
val titleOffset = content.readUInt32(84)
|
||||
val titleLength = content.readUInt32(88)
|
||||
val localeRegion = content.readUInt8(94)
|
||||
val localeLanguage = content.readUInt8(95)
|
||||
val resourceStar = content.readUInt32(108)
|
||||
val huffcdic = content.readUInt32(112)
|
||||
val numHuffcdic = content.readUInt32(116)
|
||||
val exthFlag = content.readUInt32(128)
|
||||
val trailingFlags = content.readUInt32(240)
|
||||
val indx = content.readUInt32(244)
|
||||
val charset: Charset = when (encoding) {
|
||||
65001 -> Charsets.UTF_8
|
||||
1252 -> Charset.forName("windows-1252")
|
||||
else -> error("unknown charset $encoding")
|
||||
}
|
||||
val title = content.readString(titleOffset, titleLength, charset)
|
||||
|
||||
val lang = mobiLangMap[localeLanguage]
|
||||
val language = lang?.getOrNull(localeRegion shr 2) ?: lang?.first() ?: ""
|
||||
|
||||
return MobiHeader(
|
||||
identifier, length, type, encoding, uid, version, titleOffset, titleLength,
|
||||
localeRegion, localeLanguage, resourceStar, huffcdic, numHuffcdic, exthFlag,
|
||||
trailingFlags, indx, title, language
|
||||
)
|
||||
}
|
||||
|
||||
private fun readKF8Header(content: ByteBuffer): KF8Header {
|
||||
val fdst = content.readUInt32(192)
|
||||
val numFdst = content.readUInt32(196)
|
||||
val frag = content.readUInt32(248)
|
||||
val skel = content.readUInt32(252)
|
||||
val guide = content.readUInt32(260)
|
||||
|
||||
return KF8Header(fdst, numFdst, frag, skel, guide)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val exthRecordTypeMap = mapOf(
|
||||
100 to ExthRecordType("creator", "string", true),
|
||||
101 to ExthRecordType("publisher"),
|
||||
103 to ExthRecordType("description"),
|
||||
104 to ExthRecordType("isbn"),
|
||||
105 to ExthRecordType("subject", "string", true),
|
||||
106 to ExthRecordType("date"),
|
||||
108 to ExthRecordType("contributor", "string", true),
|
||||
109 to ExthRecordType("rights"),
|
||||
110 to ExthRecordType("subjectCode", "string", true),
|
||||
112 to ExthRecordType("source", "string", true),
|
||||
113 to ExthRecordType("asin"),
|
||||
121 to ExthRecordType("boundary", "uint"),
|
||||
122 to ExthRecordType("fixedLayout"),
|
||||
125 to ExthRecordType("numResources", "uint"),
|
||||
126 to ExthRecordType("originalResolution"),
|
||||
127 to ExthRecordType("zeroGutter"),
|
||||
128 to ExthRecordType("zeroMargin"),
|
||||
129 to ExthRecordType("coverURI"),
|
||||
132 to ExthRecordType("regionMagnification"),
|
||||
201 to ExthRecordType("coverOffset", "uint"),
|
||||
202 to ExthRecordType("thumbnailOffset", "uint"),
|
||||
204 to ExthRecordType("creatorSoftware", "uint"),
|
||||
503 to ExthRecordType("title"),
|
||||
524 to ExthRecordType("language", "string", true),
|
||||
527 to ExthRecordType("pageProgressionDirection"),
|
||||
)
|
||||
|
||||
val mobiLangMap = mapOf(
|
||||
1 to listOf(
|
||||
"ar", "ar-SA", "ar-IQ", "ar-EG", "ar-LY", "ar-DZ", "ar-MA", "ar-TN", "ar-OM",
|
||||
"ar-YE", "ar-SY", "ar-JO", "ar-LB", "ar-KW", "ar-AE", "ar-BH", "ar-QA"
|
||||
),
|
||||
2 to listOf("bg"), 3 to listOf("ca"),
|
||||
4 to listOf("zh", "zh-TW", "zh-CN", "zh-HK", "zh-SG"),
|
||||
5 to listOf("cs"), 6 to listOf("da"),
|
||||
7 to listOf("de", "de-DE", "de-CH", "de-AT", "de-LU", "de-LI"), 8 to listOf("el"),
|
||||
9 to listOf(
|
||||
"en", "en-US", "en-GB", "en-AU", "en-CA", "en-NZ", "en-IE", "en-ZA",
|
||||
"en-JM", null, "en-BZ", "en-TT", "en-ZW", "en-PH"
|
||||
),
|
||||
10 to listOf(
|
||||
"es", "es-ES", "es-MX", null, "es-GT", "es-CR", "es-PA", "es-DO",
|
||||
"es-VE", "es-CO", "es-PE", "es-AR", "es-EC", "es-CL", "es-UY", "es-PY",
|
||||
"es-BO", "es-SV", "es-HN", "es-NI", "es-PR"
|
||||
),
|
||||
11 to listOf("fi"),
|
||||
12 to listOf("fr", "fr-FR", "fr-BE", "fr-CA", "fr-CH", "fr-LU", "fr-MC"),
|
||||
13 to listOf("he"), 14 to listOf("hu"), 15 to listOf("is"),
|
||||
16 to listOf("it", "it-IT", "it-CH"), 17 to listOf("ja"), 18 to listOf("ko"),
|
||||
19 to listOf("nl", "nl-NL", "nl-BE"), 20 to listOf("no", "nb", "nn"),
|
||||
21 to listOf("pl"), 22 to listOf("pt", "pt-BR", "pt-PT"), 23 to listOf("rm"),
|
||||
24 to listOf("ro"), 25 to listOf("ru"), 26 to listOf("hr", null, "sr"),
|
||||
27 to listOf("sk"), 28 to listOf("sq"), 29 to listOf("sv", "sv-SE", "sv-FI"),
|
||||
30 to listOf("th"), 31 to listOf("tr"), 32 to listOf("ur"), 33 to listOf("id"),
|
||||
34 to listOf("uk"), 35 to listOf("be"), 36 to listOf("sl"), 37 to listOf("et"),
|
||||
38 to listOf("lv"), 39 to listOf("lt"), 41 to listOf("fa"), 42 to listOf("vi"),
|
||||
43 to listOf("hy"), 44 to listOf("az"), 45 to listOf("eu"), 46 to listOf("hsb"),
|
||||
47 to listOf("mk"), 48 to listOf("st"), 49 to listOf("ts"), 50 to listOf("tn"),
|
||||
52 to listOf("xh"), 53 to listOf("zu"), 54 to listOf("af"), 55 to listOf("ka"),
|
||||
56 to listOf("fo"), 57 to listOf("hi"), 58 to listOf("mt"), 59 to listOf("se"),
|
||||
62 to listOf("ms"), 63 to listOf("kk"), 65 to listOf("sw"),
|
||||
67 to listOf("uz", null, "uz-UZ"), 68 to listOf("tt"), 69 to listOf("bn"),
|
||||
70 to listOf("pa"), 71 to listOf("gu"), 72 to listOf("or"), 73 to listOf("ta"),
|
||||
74 to listOf("te"), 75 to listOf("kn"), 76 to listOf("ml"), 77 to listOf("as"),
|
||||
78 to listOf("mr"), 79 to listOf("sa"), 82 to listOf("cy", "cy-GB"),
|
||||
83 to listOf("gl", "gl-ES"), 87 to listOf("kok"), 97 to listOf("ne"),
|
||||
98 to listOf("fy")
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.legado.app.lib.mobi
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import io.legado.app.lib.mobi.utils.readString
|
||||
import io.legado.app.lib.mobi.utils.readUInt16
|
||||
import io.legado.app.lib.mobi.utils.readUInt32
|
||||
import java.io.FileInputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
|
||||
class PDBFile(private val pfd: ParcelFileDescriptor) {
|
||||
private val fc: FileChannel = FileInputStream(pfd.fileDescriptor).channel
|
||||
private val offsets: IntArray
|
||||
val name: String
|
||||
val type: String
|
||||
val creator: String
|
||||
val recordCount: Int
|
||||
|
||||
init {
|
||||
var buffer = ByteBuffer.allocate(79)
|
||||
fc.read(buffer)
|
||||
name = buffer.readString(0, 32)
|
||||
type = buffer.readString(60, 4)
|
||||
creator = buffer.readString(64, 4)
|
||||
recordCount = buffer.readUInt16(76)
|
||||
|
||||
buffer = ByteBuffer.allocate(recordCount * 8)
|
||||
fc.read(buffer, 78)
|
||||
offsets = IntArray(recordCount) {
|
||||
buffer.readUInt32(it * 8)
|
||||
}
|
||||
}
|
||||
|
||||
fun getRecordData(index: Int): ByteBuffer {
|
||||
if (index < 0 || index >= recordCount) {
|
||||
throw IndexOutOfBoundsException("Record index out of bounds")
|
||||
}
|
||||
val len = offsets.getOrElse(index + 1) { fc.size().toInt() } - offsets[index]
|
||||
val buffer = ByteBuffer.allocate(len)
|
||||
fc.read(buffer, offsets[index].toLong())
|
||||
return buffer
|
||||
}
|
||||
|
||||
fun close() {
|
||||
fc.close()
|
||||
pfd.close()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.lib.mobi.decompress
|
||||
|
||||
class CDICEntry(
|
||||
var data: ByteArray,
|
||||
var decompressed: Boolean
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legado.app.lib.mobi.decompress
|
||||
|
||||
interface Decompressor {
|
||||
|
||||
fun decompress(data: ByteArray): ByteArray
|
||||
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package io.legado.app.lib.mobi.decompress
|
||||
|
||||
import io.legado.app.lib.mobi.MobiBook
|
||||
import io.legado.app.lib.mobi.entities.MobiHeader
|
||||
import io.legado.app.lib.mobi.utils.readIntArray
|
||||
import io.legado.app.lib.mobi.utils.readString
|
||||
import io.legado.app.lib.mobi.utils.readUInt16
|
||||
import io.legado.app.lib.mobi.utils.readUInt32
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class HuffcdicDecompressor(
|
||||
mobiBook: MobiBook,
|
||||
mobiHeader: MobiHeader,
|
||||
) : Decompressor {
|
||||
|
||||
private val magic: String
|
||||
private val offset1: Int
|
||||
private val offset2: Int
|
||||
private val table1: IntArray
|
||||
private val mincodeTable = LongArray(33)
|
||||
private val maxcodeTable = LongArray(33)
|
||||
private val dictionary = arrayListOf<CDICEntry>()
|
||||
|
||||
init {
|
||||
val huff = mobiBook.getRecord(mobiHeader.huffcdic)
|
||||
magic = huff.readString(0, 4)
|
||||
if (magic != "HUFF") error("Invalid HUFF record")
|
||||
offset1 = huff.readUInt32(8)
|
||||
offset2 = huff.readUInt32(12)
|
||||
|
||||
table1 = huff.readIntArray(offset1, 256)
|
||||
|
||||
huff.position(offset2)
|
||||
|
||||
for (i in 1..32) {
|
||||
val mincode = huff.readUInt32().toLong()
|
||||
val maxcode = huff.readUInt32().toLong()
|
||||
mincodeTable[i] = mincode shl (32 - i)
|
||||
maxcodeTable[i] = ((maxcode + 1) shl (32 - i)) - 1
|
||||
}
|
||||
|
||||
for (i in 1..<mobiHeader.numHuffcdic) {
|
||||
val record = mobiBook.getRecord(mobiHeader.huffcdic + i)
|
||||
val magic = record.readString(0, 4)
|
||||
if (magic != "CDIC") error("Invalid CDIC record")
|
||||
val length = record.readUInt32(4)
|
||||
val numEntries = record.readUInt32(8)
|
||||
val codeLength = record.readUInt32(12)
|
||||
|
||||
val n = min(1 shl codeLength, numEntries - dictionary.size)
|
||||
|
||||
record.position(length)
|
||||
val buffer = record.slice()
|
||||
for (j in 0..<n) {
|
||||
val offset = buffer.readUInt16(j * 2)
|
||||
val x = buffer.readUInt16(offset)
|
||||
val len = x and 0x7fff
|
||||
val decompressed = x and 0x8000 != 0
|
||||
val data = ByteArray(len)
|
||||
buffer.position(offset + 2)
|
||||
buffer.get(data)
|
||||
dictionary.add(CDICEntry(data, decompressed))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun decompress(data: ByteArray): ByteArray {
|
||||
val bos = ByteArrayOutputStream()
|
||||
|
||||
val buffer = ByteBuffer.wrap(data)
|
||||
|
||||
var bitsleft = data.size * 8
|
||||
var pos = 0
|
||||
var x = buffer.readUIntX(pos, 8)
|
||||
var bitcount = 32
|
||||
|
||||
while (true) {
|
||||
if (bitcount <= 0) {
|
||||
pos += 4
|
||||
x = buffer.readUIntX(pos, 8)
|
||||
bitcount += 32
|
||||
}
|
||||
|
||||
val code = (x shr bitcount) and ((1L shl 32) - 1)
|
||||
val t1 = table1[(code shr 24).toInt()]
|
||||
var codelen = t1 and 0x1f
|
||||
var maxcode = (((t1.toLong() shr 8) + 1) shl ((32L - codelen).toInt())) - 1
|
||||
|
||||
if (t1 and 0x80 == 0) {
|
||||
while (code < mincodeTable[codelen]) {
|
||||
codelen++
|
||||
}
|
||||
maxcode = maxcodeTable[codelen]
|
||||
}
|
||||
|
||||
bitcount -= codelen
|
||||
bitsleft -= codelen
|
||||
|
||||
if (bitsleft < 0) {
|
||||
break
|
||||
}
|
||||
|
||||
val index = (maxcode - code) shr ((32 - codelen))
|
||||
val entry = dictionary[index.toInt()]
|
||||
if (!entry.decompressed) {
|
||||
entry.data = decompress(entry.data)
|
||||
entry.decompressed = true
|
||||
}
|
||||
|
||||
bos.write(entry.data)
|
||||
|
||||
}
|
||||
|
||||
return bos.toByteArray()
|
||||
}
|
||||
|
||||
private fun ByteBuffer.readUIntX(offset: Int, maxlen: Int): Long {
|
||||
position(offset)
|
||||
var value = 0L
|
||||
var i = maxlen
|
||||
var bytesLeft = limit() - position()
|
||||
while (i-- > 0 && bytesLeft-- > 0) {
|
||||
value = value or ((get().toLong() and 0xFF) shl (i * 8))
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.legado.app.lib.mobi.decompress
|
||||
|
||||
import androidx.core.util.Pools.SynchronizedPool
|
||||
|
||||
class Lz77Decompressor(private val textRecordSize: Int) : Decompressor {
|
||||
|
||||
val pool = SynchronizedPool<ByteArray>(2)
|
||||
|
||||
override fun decompress(data: ByteArray): ByteArray {
|
||||
val out = pool.acquire() ?: ByteArray(textRecordSize)
|
||||
|
||||
var i = 0
|
||||
var o = 0
|
||||
while (i < data.size) {
|
||||
var c = data[i++].toInt() and 0x00FF
|
||||
if (c in 0x01..0x08) {
|
||||
var j = 0
|
||||
while (j < c && i + j < data.size) {
|
||||
out[o++] = data[i + j]
|
||||
j++
|
||||
}
|
||||
i += c
|
||||
} else if (c <= 0x7f) {
|
||||
out[o++] = c.toByte()
|
||||
} else if (c >= 0xC0) {
|
||||
out[o++] = ' '.code.toByte()
|
||||
out[o++] = (c xor 0x80).toByte()
|
||||
} else {
|
||||
if (i < data.size) {
|
||||
c = c shl 8 or (data[i++].toInt() and 0xFF)
|
||||
val length = (c and 0x0007) + 3
|
||||
val location = (c shr 3) and 0x7FF
|
||||
|
||||
if (location in 1..o) {
|
||||
for (j in 0 until length) {
|
||||
val idx = o - location
|
||||
out[o++] = out[idx]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val result = ByteArray(o)
|
||||
System.arraycopy(out, 0, result, 0, o)
|
||||
return result.also { pool.release(out) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legado.app.lib.mobi.decompress
|
||||
|
||||
class PlainDecompressor : Decompressor {
|
||||
|
||||
override fun decompress(data: ByteArray): ByteArray {
|
||||
return data
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class ExthRecordType(
|
||||
val name: String,
|
||||
val type: String = "string",
|
||||
val many: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class FdstHeader(
|
||||
val magic: String,
|
||||
val numEntries: Int
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class Fragment(
|
||||
val insertOffset: Int,
|
||||
val selector: String,
|
||||
val index: Int,
|
||||
val offset: Int,
|
||||
val length: Int
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
import android.util.SparseArray
|
||||
|
||||
data class IndexData(
|
||||
val table: List<IndexEntry>,
|
||||
val cncx: SparseArray<String>
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
import android.util.SparseArray
|
||||
|
||||
data class IndexEntry(
|
||||
val label: String,
|
||||
val tags: List<IndexTag>,
|
||||
val tagMap: SparseArray<IndexTag>
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class IndexTag(
|
||||
val tagId: Int,
|
||||
val tagValues: List<Int>
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class IndxHeader(
|
||||
val magic: String,
|
||||
val length: Int,
|
||||
val type: Int,
|
||||
val idxt: Int,
|
||||
val numRecords: Int,
|
||||
val encoding: Int,
|
||||
val language: Int,
|
||||
val total: Int,
|
||||
val ordt: Int,
|
||||
val ligt: Int,
|
||||
val numLigt: Int,
|
||||
val numCncx: Int,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class KF6Section(
|
||||
val index: Int,
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
val length: Int,
|
||||
val href: String,
|
||||
var next: KF6Section? = null
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class KF8Header(
|
||||
val fdst: Int,
|
||||
val numFdst: Int,
|
||||
val frag: Int,
|
||||
val skel: Int,
|
||||
val guide: Int,
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class KF8Pos(
|
||||
val fid: Int,
|
||||
val offset: Int
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class KF8Resource(
|
||||
val resourceType: String,
|
||||
val id: Int,
|
||||
val type: String
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class KF8Section(
|
||||
val index: Int,
|
||||
val skeleton: Skeleton,
|
||||
val frags: List<Fragment>,
|
||||
val fragEnd: Int,
|
||||
val length: Int,
|
||||
val totalLength: Int,
|
||||
val href: String,
|
||||
var next: KF8Section? = null
|
||||
) {
|
||||
val linear get() = frags.isNotEmpty()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class MobiEntryHeaders(
|
||||
val palmdoc: PalmDocHeader,
|
||||
val mobi: MobiHeader,
|
||||
val exth: Map<String, Any>,
|
||||
val kf8: KF8Header?
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class MobiHeader(
|
||||
val identifier: String,
|
||||
val length: Int,
|
||||
val type: Int,
|
||||
val encoding: Int,
|
||||
val uid: Int,
|
||||
val version: Int,
|
||||
val titleOffset: Int,
|
||||
val titleLength: Int,
|
||||
val localeRegion: Int,
|
||||
val localeLanguage: Int,
|
||||
val resourceStart: Int,
|
||||
val huffcdic: Int,
|
||||
val numHuffcdic: Int,
|
||||
val exthFlag: Int,
|
||||
val trailingFlags: Int,
|
||||
val indx: Int,
|
||||
val title: String,
|
||||
val languege: String
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class MobiMetadata(
|
||||
val identifier: String,
|
||||
val title: String,
|
||||
val author: List<String>,
|
||||
val publisher: String,
|
||||
val language: String,
|
||||
val published: String,
|
||||
val description: String,
|
||||
val subject: List<String>,
|
||||
val rights: String
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class NCX(
|
||||
val index: Int,
|
||||
val offset: Int?,
|
||||
val size: Int?,
|
||||
val label: String,
|
||||
val headingLevel: Int?,
|
||||
val pos: List<Int>?,
|
||||
val parent: Int?,
|
||||
val firstChild: Int?,
|
||||
val lastChild: Int?,
|
||||
var children: List<NCX>? = null
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class PalmDocHeader(
|
||||
val compression: Int,
|
||||
val numTextRecords: Int,
|
||||
val recordSize: Int,
|
||||
val encryption: Int
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class Ptagx(
|
||||
val tag: Int,
|
||||
val tagValueCount: Int,
|
||||
val valueCount: Int?,
|
||||
val valueBytes: Int?
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class Skeleton(
|
||||
val index: Int,
|
||||
val name: String,
|
||||
val numFrag: Int,
|
||||
val offset: Int,
|
||||
val length: Int
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class TOC(
|
||||
val label: String,
|
||||
val href: String,
|
||||
val subitems: List<TOC>? = null
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class TagxHeader(
|
||||
val magic: String,
|
||||
val length: Int,
|
||||
val numControlBytes: Int
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.lib.mobi.entities
|
||||
|
||||
data class TagxTag(
|
||||
val tag: Int,
|
||||
val numValues: Int,
|
||||
val bitmask: Int,
|
||||
val controlByte: Int,
|
||||
)
|
||||
@@ -0,0 +1,73 @@
|
||||
package io.legado.app.lib.mobi.utils
|
||||
|
||||
import okhttp3.internal.and
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
fun ByteBuffer.readByteArray(offset: Int, len: Int): ByteArray {
|
||||
position(offset)
|
||||
val b = ByteArray(len)
|
||||
get(b)
|
||||
return b
|
||||
}
|
||||
|
||||
fun ByteBuffer.readByteArray(len: Int): ByteArray {
|
||||
val b = ByteArray(len)
|
||||
get(b)
|
||||
return b
|
||||
}
|
||||
|
||||
fun ByteBuffer.readIntArray(offset: Int, len: Int): IntArray {
|
||||
position(offset)
|
||||
return IntArray(len) {
|
||||
getInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt16Array(offset: Int, len: Int): IntArray {
|
||||
position(offset)
|
||||
return IntArray(len) {
|
||||
getShort() and 0xFFFF
|
||||
}
|
||||
}
|
||||
|
||||
fun ByteBuffer.readString(len: Int): String {
|
||||
return String(readByteArray(len))
|
||||
}
|
||||
|
||||
fun ByteBuffer.readString(offset: Int, len: Int): String {
|
||||
return String(readByteArray(offset, len))
|
||||
}
|
||||
|
||||
fun ByteBuffer.readString(offset: Int, len: Int, charset: Charset): String {
|
||||
return String(readByteArray(offset, len), charset)
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt8(offset: Int): Int {
|
||||
position(offset)
|
||||
return get() and 0xFF
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt8(): Int {
|
||||
return get() and 0xFF
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt16(offset: Int): Int {
|
||||
position(offset)
|
||||
return getShort() and 0xFFFF
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt32(offset: Int): Int {
|
||||
position(offset)
|
||||
return getInt()
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt32(): Int {
|
||||
return getInt()
|
||||
}
|
||||
|
||||
fun ByteBuffer.readUInt64(offset: Int): Long {
|
||||
position(offset)
|
||||
return getLong()
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.isEpub
|
||||
import io.legado.app.help.book.isMobi
|
||||
import io.legado.app.help.book.isPdf
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.model.localBook.EpubFile
|
||||
import io.legado.app.model.localBook.MobiFile
|
||||
import io.legado.app.model.localBook.PdfFile
|
||||
import io.legado.app.utils.BitmapUtils
|
||||
import io.legado.app.utils.FileUtils
|
||||
@@ -93,22 +95,20 @@ object ImageProvider {
|
||||
return withContext(IO) {
|
||||
val vFile = BookHelp.getImage(book, src)
|
||||
if (!vFile.exists()) {
|
||||
if (book.isEpub) {
|
||||
EpubFile.getImage(book, src)?.use { input ->
|
||||
val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath)
|
||||
FileOutputStream(newFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
val inputStream = when {
|
||||
book.isEpub -> EpubFile.getImage(book, src)
|
||||
book.isPdf -> PdfFile.getImage(book, src)
|
||||
book.isMobi -> MobiFile.getImage(book, src)
|
||||
else -> {
|
||||
BookHelp.saveImage(bookSource, book, src)
|
||||
null
|
||||
}
|
||||
} else if (book.isPdf) {
|
||||
PdfFile.getImage(book, src)?.use { input ->
|
||||
val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath)
|
||||
FileOutputStream(newFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
inputStream?.use { input ->
|
||||
val newFile = FileUtils.createFileIfNotExist(vFile.absolutePath)
|
||||
FileOutputStream(newFile).use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} else {
|
||||
BookHelp.saveImage(bookSource, book, src)
|
||||
}
|
||||
}
|
||||
return@withContext vFile
|
||||
|
||||
@@ -97,6 +97,10 @@ object LocalBook {
|
||||
PdfFile.getChapterList(book)
|
||||
}
|
||||
|
||||
book.isMobi -> {
|
||||
MobiFile.getChapterList(book)
|
||||
}
|
||||
|
||||
else -> {
|
||||
TextFile.getChapterList(book)
|
||||
}
|
||||
@@ -127,6 +131,10 @@ object LocalBook {
|
||||
PdfFile.getContent(book, chapter)
|
||||
}
|
||||
|
||||
book.isMobi -> {
|
||||
MobiFile.getContent(book, chapter)
|
||||
}
|
||||
|
||||
else -> {
|
||||
TextFile.getContent(book, chapter)
|
||||
}
|
||||
@@ -208,6 +216,7 @@ object LocalBook {
|
||||
book.isEpub -> EpubFile.upBookInfo(book)
|
||||
book.isUmd -> UmdFile.upBookInfo(book)
|
||||
book.isPdf -> UmdFile.upBookInfo(book)
|
||||
book.isMobi -> MobiFile.upBookInfo(book)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package io.legado.app.model.localBook
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.ParcelFileDescriptor
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.lib.mobi.KF6Book
|
||||
import io.legado.app.lib.mobi.KF8Book
|
||||
import io.legado.app.lib.mobi.MobiBook
|
||||
import io.legado.app.lib.mobi.MobiReader
|
||||
import io.legado.app.lib.mobi.entities.TOC
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.HtmlFormatter
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import org.jsoup.Jsoup
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
|
||||
class MobiFile(var book: Book) {
|
||||
|
||||
companion object : BaseLocalBookParse {
|
||||
private var mFile: MobiFile? = null
|
||||
private val xmlDeclarationRegex = "<\\?xml[^>]*>".toRegex()
|
||||
private val doctypeDeclarationRegex = "<!DOCTYPE[^>]*>".toRegex()
|
||||
|
||||
@Synchronized
|
||||
private fun getMFile(book: Book): MobiFile {
|
||||
if (mFile == null || mFile?.book?.bookUrl != book.bookUrl) {
|
||||
mFile = MobiFile(book)
|
||||
return mFile!!
|
||||
}
|
||||
mFile?.book = book
|
||||
return mFile!!
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun getChapterList(book: Book): ArrayList<BookChapter> {
|
||||
return getMFile(book).getChapterList()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun getContent(book: Book, chapter: BookChapter): String? {
|
||||
return getMFile(book).getContent(chapter)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun getImage(book: Book, href: String): InputStream? {
|
||||
return getMFile(book).getImage(href)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun upBookInfo(book: Book) {
|
||||
return getMFile(book).upBookInfo()
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
mFile = null
|
||||
}
|
||||
}
|
||||
|
||||
private var fileDescriptor: ParcelFileDescriptor? = null
|
||||
private var mobiBook: MobiBook? = null
|
||||
get() {
|
||||
if (field == null || fileDescriptor == null) {
|
||||
field = readMobi()
|
||||
}
|
||||
return field
|
||||
}
|
||||
|
||||
private fun readMobi(): MobiBook? {
|
||||
return kotlin.runCatching {
|
||||
BookHelp.getBookPFD(book)?.let {
|
||||
fileDescriptor = it
|
||||
MobiReader().readMobi(it)
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put("读取Mobi文件失败\n${it.localizedMessage}", it)
|
||||
it.printOnDebug()
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
private fun getChapterList(): ArrayList<BookChapter> {
|
||||
return when (val book = mobiBook) {
|
||||
is KF8Book -> getChapterListKF8(book)
|
||||
is KF6Book -> getChapterListKF6(book)
|
||||
else -> error("impossible condition")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChapterListKF6(kF6Book: KF6Book): ArrayList<BookChapter> {
|
||||
val chapterList = arrayListOf<BookChapter>()
|
||||
val toc = kF6Book.toc
|
||||
|
||||
if (kF6Book.sectionIdMap[0] == null) {
|
||||
val section = kF6Book.sections.firstOrNull()
|
||||
if (section != null) {
|
||||
val chapter = BookChapter()
|
||||
val content = kF6Book.getSectionText(section)
|
||||
val soup = Jsoup.parse(content)
|
||||
val title = soup.getElementsByTag("title").first()?.text() ?: "卷首"
|
||||
chapter.bookUrl = book.bookUrl
|
||||
chapter.title = title
|
||||
chapter.url = "0:" + section.href
|
||||
chapterList.add(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun append(ref: TOC) {
|
||||
val chapter = BookChapter()
|
||||
chapter.bookUrl = book.bookUrl
|
||||
chapter.title = ref.label
|
||||
chapter.url = "${chapterList.size}:${ref.href}"
|
||||
chapter.isVolume = ref.subitems != null
|
||||
val lastChapter = chapterList.lastOrNull()
|
||||
if (lastChapter != null &&
|
||||
lastChapter.isVolume &&
|
||||
lastChapter.url.substringAfter(":") == chapter.url.substringAfter(":")
|
||||
) {
|
||||
lastChapter.url = "skip:" + lastChapter.url
|
||||
}
|
||||
lastChapter?.putVariable("nextUrl", chapter.url)
|
||||
chapterList.add(chapter)
|
||||
ref.subitems?.forEach(::append)
|
||||
}
|
||||
|
||||
toc?.forEach(::append)
|
||||
|
||||
return chapterList
|
||||
}
|
||||
|
||||
private fun getChapterListKF8(kf8Book: KF8Book): ArrayList<BookChapter> {
|
||||
val chapterList = arrayListOf<BookChapter>()
|
||||
val toc = kf8Book.toc
|
||||
|
||||
if (kf8Book.sectionIdMap[0] == null) {
|
||||
val section = kf8Book.sections.firstOrNull { it.href.isNotEmpty() }
|
||||
if (section != null) {
|
||||
val chapter = BookChapter()
|
||||
val content = kf8Book.getSectionText(section)
|
||||
val soup = Jsoup.parse(content)
|
||||
val title = soup.getElementsByTag("title").first()?.text() ?: "卷首"
|
||||
chapter.bookUrl = book.bookUrl
|
||||
chapter.title = title
|
||||
chapter.url = "0:" + section.href
|
||||
chapterList.add(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
fun append(ref: TOC) {
|
||||
val chapter = BookChapter()
|
||||
chapter.bookUrl = book.bookUrl
|
||||
chapter.title = ref.label
|
||||
chapter.url = "${chapterList.size}:${ref.href}"
|
||||
chapter.isVolume = ref.subitems != null
|
||||
val lastChapter = chapterList.lastOrNull()
|
||||
if (lastChapter != null &&
|
||||
lastChapter.isVolume &&
|
||||
lastChapter.url.substringAfter(":") == chapter.url.substringAfter(":")
|
||||
) {
|
||||
lastChapter.url = "skip:" + lastChapter.url
|
||||
}
|
||||
lastChapter?.putVariable("nextUrl", chapter.url)
|
||||
chapterList.add(chapter)
|
||||
ref.subitems?.forEach(::append)
|
||||
}
|
||||
|
||||
toc?.forEach(::append)
|
||||
|
||||
return chapterList
|
||||
}
|
||||
|
||||
private fun getContent(chapter: BookChapter): String? {
|
||||
return when (val book = mobiBook) {
|
||||
is KF8Book -> getContentKF8(book, chapter)
|
||||
is KF6Book -> getContentKF6(book, chapter)
|
||||
else -> error("impossible condition")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getContentKF6(kf6Book: KF6Book, chapter: BookChapter): String? {
|
||||
if (chapter.isVolume && chapter.url.startsWith("skip:")) return ""
|
||||
var section = kf6Book.getSectionByHref(chapter.url) ?: return null
|
||||
val nextSectionHref = chapter.getVariable("nextUrl")
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.append(kf6Book.getSectionText(section))
|
||||
while (true) {
|
||||
section = section.next ?: break
|
||||
if (section.href == nextSectionHref) {
|
||||
break
|
||||
}
|
||||
if (kf6Book.sectionIdMap[section.index] != null) {
|
||||
break
|
||||
}
|
||||
sb.append(kf6Book.getSectionText(section))
|
||||
}
|
||||
|
||||
val soup = Jsoup.parse(sb.toString())
|
||||
|
||||
soup.select("title").remove()
|
||||
soup.select("[style*=display:none]").remove()
|
||||
soup.select("img[recindex]").forEach {
|
||||
val recindex = it.attr("recindex")
|
||||
it.clearAttributes()
|
||||
it.attr("src", "recindex:$recindex")
|
||||
}
|
||||
|
||||
return format(soup.outerHtml())
|
||||
}
|
||||
|
||||
private fun getContentKF8(kf8Book: KF8Book, chapter: BookChapter): String? {
|
||||
if (chapter.isVolume && chapter.url.startsWith("skip:")) return ""
|
||||
var section = kf8Book.getSectionByHref(chapter.url) ?: return null
|
||||
val nextSectionHref = chapter.getVariable("nextUrl")
|
||||
val nextPos = kf8Book.parsePosURI(nextSectionHref)
|
||||
|
||||
val sb = StringBuilder()
|
||||
sb.append(kf8Book.getTextByHref(chapter.url, nextSectionHref))
|
||||
while (true) {
|
||||
if (nextPos != null && section.frags.any { it.index == nextPos.fid }) {
|
||||
break
|
||||
}
|
||||
section = section.next ?: break
|
||||
if (section.linear) {
|
||||
continue
|
||||
}
|
||||
if (section.href == nextSectionHref) {
|
||||
break
|
||||
}
|
||||
if (kf8Book.sectionIdMap[section.index] != null) {
|
||||
break
|
||||
}
|
||||
sb.append(kf8Book.getSectionText(section))
|
||||
}
|
||||
|
||||
val soup = Jsoup.parse(sb.toString())
|
||||
|
||||
soup.select("title").remove()
|
||||
soup.select("[style*=display:none]").remove()
|
||||
|
||||
return format(soup.outerHtml())
|
||||
}
|
||||
|
||||
private fun format(html: String): String {
|
||||
return HtmlFormatter.formatKeepImg(html)
|
||||
.replace(xmlDeclarationRegex, "")
|
||||
.replace(doctypeDeclarationRegex, "")
|
||||
}
|
||||
|
||||
private fun getImage(href: String): InputStream? {
|
||||
return when (val book = mobiBook) {
|
||||
is KF8Book -> getImageKF8(book, href)
|
||||
is KF6Book -> getImageKF6(book, href)
|
||||
else -> error("impossible condition")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getImageKF6(kf6Book: KF6Book, href: String): InputStream? {
|
||||
return kf6Book.getResourceByHref(href)?.inputStream()
|
||||
}
|
||||
|
||||
private fun getImageKF8(kf8Book: KF8Book, href: String): InputStream? {
|
||||
return kf8Book.getResourceByHref(href)?.inputStream()
|
||||
}
|
||||
|
||||
private fun upBookCover() {
|
||||
try {
|
||||
mobiBook?.let {
|
||||
if (book.coverUrl.isNullOrEmpty()) {
|
||||
book.coverUrl = LocalBook.getCoverPath(book)
|
||||
}
|
||||
it.getCover()?.let { bytes ->
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
val file = FileUtils.createFileIfNotExist(book.coverUrl!!)
|
||||
FileOutputStream(file).use { out ->
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("加载书籍封面失败\n${e.localizedMessage}", e)
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
|
||||
private fun upBookInfo() {
|
||||
if (mobiBook == null) {
|
||||
mFile = null
|
||||
book.intro = "书籍导入异常"
|
||||
} else {
|
||||
upBookCover()
|
||||
val metadata = mobiBook!!.metadata
|
||||
book.name = metadata.title
|
||||
if (book.name.isEmpty()) {
|
||||
book.name = book.originName.replace("(?i)\\.(mobi|azw3)$".toRegex(), "")
|
||||
}
|
||||
if (metadata.author.isNotEmpty()) {
|
||||
book.author = metadata.author.first()
|
||||
}
|
||||
if (metadata.description.isNotBlank()) {
|
||||
book.intro = HtmlFormatter.format(metadata.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -45,6 +45,7 @@ import io.legado.app.help.book.isAudio
|
||||
import io.legado.app.help.book.isEpub
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.help.book.isLocalTxt
|
||||
import io.legado.app.help.book.isMobi
|
||||
import io.legado.app.help.book.removeType
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
@@ -59,6 +60,7 @@ import io.legado.app.model.ReadAloud
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||
import io.legado.app.model.localBook.EpubFile
|
||||
import io.legado.app.model.localBook.MobiFile
|
||||
import io.legado.app.receiver.TimeBatteryReceiver
|
||||
import io.legado.app.service.BaseReadAloudService
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
@@ -480,6 +482,9 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
BookHelp.clearCache(it)
|
||||
EpubFile.clear()
|
||||
}
|
||||
if (it.isMobi) {
|
||||
MobiFile.clear()
|
||||
}
|
||||
loadChapterList(it)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user