mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Reader: allow opening unzipped EPUBs
This commit is contained in:
@@ -66,15 +66,17 @@ Almost all of the properties and methods are optional. At minimum it needs `.sec
|
||||
|
||||
### Archived Files
|
||||
|
||||
Reading Zip-based formats requires a separate library. Both `epub.js` and `comic-book.js` expect a `loader` object that implements the following interface:
|
||||
Reading Zip-based formats will require adapting an external library. Both `epub.js` and `comic-book.js` expect a `loader` object that implements the following interface:
|
||||
|
||||
- `.entries`: an array, each element of which has a `filename` property, which is a string containing the filename (the full path) (only used by `comic-book.js`)
|
||||
- `.loadText(filename)`: given the path, returns content of the file as string
|
||||
- `.loadBlob(filename)`: give the path, returns the file as a blob
|
||||
- `.getSize(filename)`: returns the file size in bytes
|
||||
- `.entries`: (only used by `comic-book.js`) an array, each element of which has a `filename` property, which is a string containing the filename (the full path).
|
||||
- `.loadText(filename)`: given the path, returns the contents of the file as string. May be async.
|
||||
- `.loadBlob(filename)`: given the path, returns the file as a `Blob` object. May be async.
|
||||
- `.getSize(filename)`: returns the file size in bytes. Used to set the `.size` property for `.sections` (see above).
|
||||
|
||||
In the demo, this is implemented using [Zip.js](https://github.com/gildas-lormeau/zip.js), which is highly recommended because it seems to be the only library that supports random access for `File` objects (as well as HTTP range requests).
|
||||
|
||||
One advantage of having such an interface is that one can easily use it for reading unarchived files as well. For example, the demo has a loader that allows you to open unpacked EPUBs as directories.
|
||||
|
||||
### Mobipocket and Kindle Files
|
||||
|
||||
It can read both MOBI and KF8 (.azw3, and combo .mobi files) from a `File` (or `Blob`) object. For MOBI files, it decompresses all text at once and splits the raw markup into sections at every `<mbp:pagebreak>`, instead of outputing one long page for the whole book, which drastically improves rendering performance. For KF8 files, it tries to decompress as little text as possible when loading a section, but it can still be quite slow due to the slowness of the current HUFF/CDIC decompressor implementation. In all cases, images and other resources are not loaded until they are needed.
|
||||
|
||||
@@ -604,7 +604,7 @@ export class EPUB {
|
||||
})
|
||||
const loader = new Loader({
|
||||
loadText: this.loadText,
|
||||
loadBlob: uri => this.loadBlob(uri)
|
||||
loadBlob: uri => Promise.resolve(this.loadBlob(uri))
|
||||
.then(this.#encryption.getDecoder(uri)),
|
||||
resources: this.resources,
|
||||
})
|
||||
|
||||
@@ -24,6 +24,29 @@ const makeZipLoader = async file => {
|
||||
return { entries, loadText, loadBlob, getSize }
|
||||
}
|
||||
|
||||
const getFileEntries = async entry => entry.isFile ? entry
|
||||
: (await Promise.all(Array.from(
|
||||
await new Promise((resolve, reject) => entry.createReader()
|
||||
.readEntries(entries => resolve(entries), error => reject(error))),
|
||||
getFileEntries))).flat()
|
||||
|
||||
const makeDirectoryLoader = async entry => {
|
||||
const entries = await getFileEntries(entry)
|
||||
const files = await Promise.all(
|
||||
entries.map(entry => new Promise((resolve, reject) =>
|
||||
entry.file(file => resolve([file, entry.fullPath]),
|
||||
error => reject(error)))))
|
||||
const map = new Map(files.map(([file, path]) =>
|
||||
[path.replace(entry.fullPath + '/', ''), file]))
|
||||
const decoder = new TextDecoder()
|
||||
const decode = x => x ? decoder.decode(x) : null
|
||||
const getBuffer = name => map.get(name)?.arrayBuffer() ?? null
|
||||
const loadText = async name => decode(await getBuffer(name))
|
||||
const loadBlob = name => map.get(name)
|
||||
const getSize = name => map.get(name)?.size ?? 0
|
||||
return { loadText, loadBlob, getSize }
|
||||
}
|
||||
|
||||
const isCBZ = ({ name, type }) =>
|
||||
type === 'application/vnd.comicbook+zip' || name.endsWith('.cbz')
|
||||
|
||||
@@ -35,9 +58,14 @@ const isFBZ = ({ name, type }) =>
|
||||
|| name.endsWith('.fb2.zip') || name.endsWith('.fbz')
|
||||
|
||||
const getView = async (file, emit) => {
|
||||
if (!file.size) throw new Error('File not found')
|
||||
let book
|
||||
if (await isZip(file)) {
|
||||
if (file.isDirectory) {
|
||||
const loader = await makeDirectoryLoader(file)
|
||||
const { EPUB } = await import('./epub.js')
|
||||
book = await new EPUB(loader).init()
|
||||
}
|
||||
else if (!file.size) throw new Error('File not found')
|
||||
else if (await isZip(file)) {
|
||||
const loader = await makeZipLoader(file)
|
||||
if (isCBZ(file)) {
|
||||
const { makeComicBook } = await import('./comic-book.js')
|
||||
@@ -195,7 +223,7 @@ class Reader {
|
||||
?.map(author => typeof author === 'string' ? author : author.name)
|
||||
?.join(', ')
|
||||
?? ''
|
||||
book.getCover?.()?.then(blob =>
|
||||
Promise.resolve(book.getCover?.())?.then(blob =>
|
||||
blob ? $('#side-bar-cover').src = URL.createObjectURL(blob) : null)
|
||||
|
||||
const toc = book.toc
|
||||
@@ -276,9 +304,12 @@ const open = async file => {
|
||||
const dragOverHandler = e => e.preventDefault()
|
||||
const dropHandler = e => {
|
||||
e.preventDefault()
|
||||
const file = Array.from(e.dataTransfer.items)
|
||||
.find(item => item.kind === 'file')?.getAsFile()
|
||||
if (file) open(file).catch(e => console.error(e))
|
||||
const item = Array.from(e.dataTransfer.items)
|
||||
.find(item => item.kind === 'file')
|
||||
if (item) {
|
||||
const entry = item.webkitGetAsEntry()
|
||||
open(entry.isFile ? item.getAsFile() : entry).catch(e => console.error(e))
|
||||
}
|
||||
}
|
||||
const dropTarget = $('#drop-target')
|
||||
dropTarget.addEventListener('drop', dropHandler)
|
||||
|
||||
Reference in New Issue
Block a user