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
|
### 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`)
|
- `.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 content of the file as string
|
- `.loadText(filename)`: given the path, returns the contents of the file as string. May be async.
|
||||||
- `.loadBlob(filename)`: give the path, returns the file as a blob
|
- `.loadBlob(filename)`: given the path, returns the file as a `Blob` object. May be async.
|
||||||
- `.getSize(filename)`: returns the file size in bytes
|
- `.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).
|
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
|
### 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.
|
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({
|
const loader = new Loader({
|
||||||
loadText: this.loadText,
|
loadText: this.loadText,
|
||||||
loadBlob: uri => this.loadBlob(uri)
|
loadBlob: uri => Promise.resolve(this.loadBlob(uri))
|
||||||
.then(this.#encryption.getDecoder(uri)),
|
.then(this.#encryption.getDecoder(uri)),
|
||||||
resources: this.resources,
|
resources: this.resources,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,29 @@ const makeZipLoader = async file => {
|
|||||||
return { entries, loadText, loadBlob, getSize }
|
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 }) =>
|
const isCBZ = ({ name, type }) =>
|
||||||
type === 'application/vnd.comicbook+zip' || name.endsWith('.cbz')
|
type === 'application/vnd.comicbook+zip' || name.endsWith('.cbz')
|
||||||
|
|
||||||
@@ -35,9 +58,14 @@ const isFBZ = ({ name, type }) =>
|
|||||||
|| name.endsWith('.fb2.zip') || name.endsWith('.fbz')
|
|| name.endsWith('.fb2.zip') || name.endsWith('.fbz')
|
||||||
|
|
||||||
const getView = async (file, emit) => {
|
const getView = async (file, emit) => {
|
||||||
if (!file.size) throw new Error('File not found')
|
|
||||||
let book
|
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)
|
const loader = await makeZipLoader(file)
|
||||||
if (isCBZ(file)) {
|
if (isCBZ(file)) {
|
||||||
const { makeComicBook } = await import('./comic-book.js')
|
const { makeComicBook } = await import('./comic-book.js')
|
||||||
@@ -195,7 +223,7 @@ class Reader {
|
|||||||
?.map(author => typeof author === 'string' ? author : author.name)
|
?.map(author => typeof author === 'string' ? author : author.name)
|
||||||
?.join(', ')
|
?.join(', ')
|
||||||
?? ''
|
?? ''
|
||||||
book.getCover?.()?.then(blob =>
|
Promise.resolve(book.getCover?.())?.then(blob =>
|
||||||
blob ? $('#side-bar-cover').src = URL.createObjectURL(blob) : null)
|
blob ? $('#side-bar-cover').src = URL.createObjectURL(blob) : null)
|
||||||
|
|
||||||
const toc = book.toc
|
const toc = book.toc
|
||||||
@@ -276,9 +304,12 @@ const open = async file => {
|
|||||||
const dragOverHandler = e => e.preventDefault()
|
const dragOverHandler = e => e.preventDefault()
|
||||||
const dropHandler = e => {
|
const dropHandler = e => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const file = Array.from(e.dataTransfer.items)
|
const item = Array.from(e.dataTransfer.items)
|
||||||
.find(item => item.kind === 'file')?.getAsFile()
|
.find(item => item.kind === 'file')
|
||||||
if (file) open(file).catch(e => console.error(e))
|
if (item) {
|
||||||
|
const entry = item.webkitGetAsEntry()
|
||||||
|
open(entry.isFile ? item.getAsFile() : entry).catch(e => console.error(e))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const dropTarget = $('#drop-target')
|
const dropTarget = $('#drop-target')
|
||||||
dropTarget.addEventListener('drop', dropHandler)
|
dropTarget.addEventListener('drop', dropHandler)
|
||||||
|
|||||||
Reference in New Issue
Block a user