fix(ui): render PDFs with pdf.js instead of an iframe
Nightly Build / build (push) Successful in 7m59s
Nightly Build / build (push) Successful in 7m59s
On iOS the file viewer showed only the first page of a PDF, with no way to scroll to the rest — the document had to be downloaded and opened in another app. The cause was not ours: WebKit refuses to mount its PDF viewer inside an <iframe>/<object>/<embed> and paints a static first-page thumbnail instead. That hits Safari on iOS and every WKWebView, so the native shell too. The full viewer only exists for a top-level navigation. The desktop browsers do mount a viewer, but each mounts its own — Chrome's toolbar, Safari's page-index sidebar — so the same document also looked different on every machine. Both are answered by drawing the pages ourselves. New <pdf-view> (web/components/shared/pdf-view.js) renders a continuous scroll of canvas pages on the vendored pdf.js, with a zoom control and a page counter, and replaces the iframe for both native .pdf files and server-compiled LaTeX. Three properties are load-bearing: - pdf.js is imported lazily (~450 KB + a 1.2 MB worker), so a session that never opens a PDF never pays for it. - Canvases are created and destroyed as pages scroll. iOS caps the total canvas backing store a page may hold and silently blanks canvases past it, so an eager render would come out empty on exactly the platform this was written for. Off-screen pages keep only a correctly-sized box, which is also what keeps the scrollbar honest. - The text layer (selection, in-page find) is best-effort: it is transparent DOM over the pixels, so its failures are swallowed rather than surfaced. Vendored from pdfjs-dist 6.2.108: pdf.min.mjs, pdf.worker.min.mjs, the standard-font data (needed by PDFs that reference Helvetica/Times without embedding them) and the .textLayer block of pdf_viewer.css. CJK cmaps are deliberately left out. pdf.js 6 needs Safari/iOS 17.4+. Verified in headless Chromium against both a synthetic 12-page PDF using non-embedded Helvetica and a real 14-page paper with embedded fonts and figures: all pages present, last page renders after scrolling, page 1 released off-screen, text layer populated, zoom re-renders at the new scale, no JS errors.
This commit is contained in:
@@ -4,6 +4,7 @@ import { keyed } from 'lit/directives/keyed.js';
|
||||
import { LightElement, renderMarkdown } from '../../lib/base.js';
|
||||
import { fileWatcher } from '../../lib/file-watcher.js';
|
||||
import { t } from '../../lib/i18n.js';
|
||||
import './pdf-view.js'; // registers <pdf-view>; pdf.js itself is imported lazily
|
||||
|
||||
/**
|
||||
* Shared file-viewer engine. Holds all of the fetch / kind-detection /
|
||||
@@ -554,17 +555,20 @@ export class FileViewerBase extends LightElement {
|
||||
return html`<div class="fv-image-wrap"><img src=${this._blobUrl} alt=${this._path} class="fv-image" /></div>`;
|
||||
}
|
||||
if (this._kind === 'pdf' && this._blobUrl) {
|
||||
// `keyed` re-creates the iframe element on every new blob URL: the first
|
||||
// navigation of a fresh iframe replaces its history slot instead of
|
||||
// pushing one — whereas re-assigning `src` on an existing iframe pushes
|
||||
// a joint session-history entry each time (during the watch-reload loop
|
||||
// that buried the back button under hundreds of blob: entries).
|
||||
return keyed(this._blobUrl, html`<iframe class="fv-pdf" src=${this._blobUrl} title=${this._path}></iframe>`);
|
||||
// Drawn by <pdf-view> (pdf.js on canvas), never by the browser's built-in
|
||||
// viewer in an <iframe>: WebKit renders a framed PDF as a static first-page
|
||||
// thumbnail, so on iOS — Safari and every WKWebView, the native shell
|
||||
// included — the document had one page and no scroll. It also removes the
|
||||
// per-browser viewer chrome (Chrome's toolbar, Safari's page sidebar), so
|
||||
// a PDF now looks the same everywhere. `keyed` is gone with the iframe:
|
||||
// updating a property pushes no session-history entry, which is what the
|
||||
// watch-reload loop used to bury the back button under blob: entries.
|
||||
return html`<pdf-view class="fv-pdf" .src=${this._blobUrl}></pdf-view>`;
|
||||
}
|
||||
if (this._kind === 'latex' && this._blobUrl) {
|
||||
// Successfully compiled server-side — render the resulting PDF the same
|
||||
// way a native .pdf would be rendered (see the keyed() note above).
|
||||
return keyed(this._blobUrl, html`<iframe class="fv-pdf" src=${this._blobUrl} title=${this._path}></iframe>`);
|
||||
// Successfully compiled server-side — render the resulting PDF exactly as
|
||||
// a native .pdf is rendered (see the note above).
|
||||
return html`<pdf-view class="fv-pdf" .src=${this._blobUrl}></pdf-view>`;
|
||||
}
|
||||
if (this._kind === 'svg' && this._blobUrl) {
|
||||
// `allow-same-origin` (and nothing else) is required so the iframe can load
|
||||
|
||||
Reference in New Issue
Block a user