mirror of
https://github.com/john-okeefe/foliate-js.git
synced 2026-09-09 11:29:14 -04:00
Add PDF support
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
Library for rendering e-books in the browser.
|
Library for rendering e-books in the browser.
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Supports EPUB, MOBI, KF8, FB2, CBZ
|
- Supports EPUB, MOBI, KF8 (AZW3), FB2, CBZ, PDF (experimental; requires PDF.js), or add support for other formats yourself by implementing the book interface
|
||||||
- Pure JavaScript
|
- Pure JavaScript
|
||||||
- Small and modular
|
- Small and modular
|
||||||
- No dependencies
|
- No dependencies
|
||||||
@@ -128,6 +128,12 @@ It can read both MOBI and KF8 (.azw3, and combo .mobi files) from a `File` (or `
|
|||||||
|
|
||||||
Note that KF8 files can contain fonts that are zlib-compressed. They need to be decompressed with an external library. The demo uses [fflate](https://github.com/101arrowz/fflate) to decompress them.
|
Note that KF8 files can contain fonts that are zlib-compressed. They need to be decompressed with an external library. The demo uses [fflate](https://github.com/101arrowz/fflate) to decompress them.
|
||||||
|
|
||||||
|
### PDF and Other Fixed-Layout Formats
|
||||||
|
|
||||||
|
There is a proof-of-concept, highly experimental adapter for [PDF.js](https://mozilla.github.io/pdf.js/), with which you can show PDFs using the same fixed-layout renderer for EPUBs.
|
||||||
|
|
||||||
|
CBZs are similarly handled like fixed-layout EPUBs.
|
||||||
|
|
||||||
### The Renderers
|
### The Renderers
|
||||||
|
|
||||||
It has two renderers, one for paginating reflowable books, and one for fixed-layout. They are custom elements (web components).
|
It has two renderers, one for paginating reflowable books, and one for fixed-layout. They are custom elements (web components).
|
||||||
@@ -292,3 +298,4 @@ MIT.
|
|||||||
Vendored libraries for the demo:
|
Vendored libraries for the demo:
|
||||||
- [zip.js](https://github.com/gildas-lormeau/zip.js) is licensed under the BSD-3-Clause license.
|
- [zip.js](https://github.com/gildas-lormeau/zip.js) is licensed under the BSD-3-Clause license.
|
||||||
- [fflate](https://github.com/101arrowz/fflate) is MIT licensed.
|
- [fflate](https://github.com/101arrowz/fflate) is MIT licensed.
|
||||||
|
- [PDF.js](https://mozilla.github.io/pdf.js/) is licensed under Apache.
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/* global pdfjsLib */
|
||||||
|
|
||||||
|
// https://github.com/mozilla/pdf.js/blob/f04967017f22e46d70d11468dd928b4cdc2f6ea1/web/text_layer_builder.css
|
||||||
|
const textLayerBuilderCSS = `
|
||||||
|
/* Copyright 2014 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--highlight-bg-color: rgb(180 0 170);
|
||||||
|
--highlight-selected-bg-color: rgb(0 100 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (forced-colors: active) {
|
||||||
|
:root {
|
||||||
|
--highlight-bg-color: Highlight;
|
||||||
|
--highlight-selected-bg-color: ButtonText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer {
|
||||||
|
position: absolute;
|
||||||
|
text-align: initial;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0.25;
|
||||||
|
line-height: 1;
|
||||||
|
text-size-adjust: none;
|
||||||
|
forced-color-adjust: none;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer :is(span, br) {
|
||||||
|
color: transparent;
|
||||||
|
position: absolute;
|
||||||
|
white-space: pre;
|
||||||
|
cursor: text;
|
||||||
|
transform-origin: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only necessary in Google Chrome, see issue 14205, and most unfortunately
|
||||||
|
* the problem doesn't show up in "text" reference tests. */
|
||||||
|
/*#if !MOZCENTRAL*/
|
||||||
|
.textLayer span.markedContent {
|
||||||
|
top: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
/*#endif*/
|
||||||
|
|
||||||
|
.textLayer .highlight {
|
||||||
|
margin: -1px;
|
||||||
|
padding: 1px;
|
||||||
|
background-color: var(--highlight-bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer .highlight.appended {
|
||||||
|
position: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer .highlight.begin {
|
||||||
|
border-radius: 4px 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer .highlight.end {
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer .highlight.middle {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer .highlight.selected {
|
||||||
|
background-color: var(--highlight-selected-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer ::selection {
|
||||||
|
/*#if !MOZCENTRAL*/
|
||||||
|
background: blue;
|
||||||
|
/*#endif*/
|
||||||
|
background: AccentColor; /* stylelint-disable-line declaration-block-no-duplicate-properties */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Avoids https://github.com/mozilla/pdf.js/issues/13840 in Chrome */
|
||||||
|
/*#if !MOZCENTRAL*/
|
||||||
|
.textLayer br::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/*#endif*/
|
||||||
|
|
||||||
|
.textLayer .endOfContent {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
inset: 100% 0 0;
|
||||||
|
z-index: -1;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textLayer .endOfContent.active {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const renderPage = async (page, getImageBlob) => {
|
||||||
|
const scale = devicePixelRatio
|
||||||
|
const viewport = page.getViewport({ scale })
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.height = viewport.height
|
||||||
|
canvas.width = viewport.width
|
||||||
|
const canvasContext = canvas.getContext('2d')
|
||||||
|
await page.render({ canvasContext, viewport }).promise
|
||||||
|
const blob = await new Promise(resolve => canvas.toBlob(resolve))
|
||||||
|
if (getImageBlob) return blob
|
||||||
|
|
||||||
|
/*
|
||||||
|
// with the SVG backend
|
||||||
|
const operatorList = await page.getOperatorList()
|
||||||
|
const svgGraphics = new pdfjsLib.SVGGraphics(page.commonObjs, page.objs)
|
||||||
|
const svg = await svgGraphics.getSVG(operatorList, viewport)
|
||||||
|
const str = new XMLSerializer().serializeToString(svg)
|
||||||
|
const blob = new Blob([str], { type: 'image/svg+xml' })
|
||||||
|
*/
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.classList.add('textLayer')
|
||||||
|
await pdfjsLib.renderTextLayer({
|
||||||
|
textContentSource: await page.getTextContent(),
|
||||||
|
container, viewport,
|
||||||
|
}).promise
|
||||||
|
|
||||||
|
const src = URL.createObjectURL(blob)
|
||||||
|
const url = URL.createObjectURL(new Blob([`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--scale-factor: ${scale};
|
||||||
|
}
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
${textLayerBuilderCSS}
|
||||||
|
</style>
|
||||||
|
<img src="${src}">
|
||||||
|
${container.outerHTML}
|
||||||
|
`], { type: 'text/html' }))
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeTOCItem = item => ({
|
||||||
|
label: item.title,
|
||||||
|
href: JSON.stringify(item.dest),
|
||||||
|
subitems: item.items.length ? item.items.map(makeTOCItem) : null,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const makePDF = async file => {
|
||||||
|
const data = new Uint8Array(await file.arrayBuffer())
|
||||||
|
const pdf = await pdfjsLib.getDocument({ data }).promise
|
||||||
|
|
||||||
|
const book = { rendition: { layout: 'pre-paginated' } }
|
||||||
|
|
||||||
|
const info = (await pdf.getMetadata())?.info
|
||||||
|
book.metadata = {
|
||||||
|
title: info?.Title,
|
||||||
|
author: info?.Author,
|
||||||
|
}
|
||||||
|
|
||||||
|
const outline = await pdf.getOutline()
|
||||||
|
book.toc = outline?.map(makeTOCItem)
|
||||||
|
|
||||||
|
const cache = new Map()
|
||||||
|
book.sections = Array.from({ length: pdf.numPages }).map((_, i) => ({
|
||||||
|
id: i,
|
||||||
|
load: async () => {
|
||||||
|
const cached = cache.get(i)
|
||||||
|
if (cached) return cached
|
||||||
|
const url = await renderPage(await pdf.getPage(i + 1))
|
||||||
|
cache.set(i, url)
|
||||||
|
return url
|
||||||
|
},
|
||||||
|
size: 1000,
|
||||||
|
}))
|
||||||
|
book.resolveHref = async href => {
|
||||||
|
const parsed = JSON.parse(href)
|
||||||
|
const dest = typeof parsed === 'string'
|
||||||
|
? await pdf.getDestination(parsed) : parsed
|
||||||
|
const index = await pdf.getPageIndex(dest[0])
|
||||||
|
return { index }
|
||||||
|
}
|
||||||
|
book.splitTOCHref = async href => {
|
||||||
|
const parsed = JSON.parse(href)
|
||||||
|
const dest = typeof parsed === 'string'
|
||||||
|
? await pdf.getDestination(parsed) : parsed
|
||||||
|
const index = await pdf.getPageIndex(dest[0])
|
||||||
|
return [index, null]
|
||||||
|
}
|
||||||
|
book.getTOCFragment = doc => doc.documentElement
|
||||||
|
book.getCover = async () => renderPage(await pdf.getPage(1), true)
|
||||||
|
return book
|
||||||
|
}
|
||||||
@@ -301,4 +301,5 @@ body {
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="vendor/pdfjs/pdf.js"></script>
|
||||||
<script src="reader.js" type="module"></script>
|
<script src="reader.js" type="module"></script>
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ const isZip = async file => {
|
|||||||
return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04
|
return arr[0] === 0x50 && arr[1] === 0x4b && arr[2] === 0x03 && arr[3] === 0x04
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPDF = async file => {
|
||||||
|
const arr = new Uint8Array(await file.slice(0, 5).arrayBuffer())
|
||||||
|
return arr[0] === 0x25
|
||||||
|
&& arr[1] === 0x50 && arr[2] === 0x44 && arr[3] === 0x46
|
||||||
|
&& arr[4] === 0x2d
|
||||||
|
}
|
||||||
|
|
||||||
const makeZipLoader = async file => {
|
const makeZipLoader = async file => {
|
||||||
const { configure, ZipReader, BlobReader, TextWriter, BlobWriter } =
|
const { configure, ZipReader, BlobReader, TextWriter, BlobWriter } =
|
||||||
await import('./vendor/zip.js')
|
await import('./vendor/zip.js')
|
||||||
@@ -79,7 +86,12 @@ const getView = async file => {
|
|||||||
const { EPUB } = await import('./epub.js')
|
const { EPUB } = await import('./epub.js')
|
||||||
book = await new EPUB(loader).init()
|
book = await new EPUB(loader).init()
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
else if (await isPDF(file)) {
|
||||||
|
const { makePDF } = await import('./pdf.js')
|
||||||
|
book = await makePDF(file)
|
||||||
|
}
|
||||||
|
else {
|
||||||
const { isMOBI, MOBI } = await import('./mobi.js')
|
const { isMOBI, MOBI } = await import('./mobi.js')
|
||||||
if (await isMOBI(file)) {
|
if (await isMOBI(file)) {
|
||||||
const fflate = await import('./vendor/fflate.js')
|
const fflate = await import('./vendor/fflate.js')
|
||||||
|
|||||||
Vendored
+18146
File diff suppressed because it is too large
Load Diff
Vendored
+58353
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user