Reader: allow opening unzipped EPUBs

This commit is contained in:
John Factotum
2022-10-27 17:41:26 +00:00
committed by GitHub
parent 60bebc39b4
commit 2a5ccb93d9
3 changed files with 45 additions and 12 deletions
+7 -5
View File
@@ -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.
+1 -1
View File
@@ -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,
}) })
+37 -6
View File
@@ -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)