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.
153 lines
3.4 KiB
CSS
153 lines
3.4 KiB
CSS
/* pdf.js text-layer styles — extracted verbatim from pdfjs-dist@6.2.108
|
|
web/pdf_viewer.css (.textLayer block only; the rest of that file styles
|
|
the full pdf.js viewer app, which we don't use). Keep in sync when the
|
|
vendored pdf.min.mjs is upgraded — TextLayer's DOM contract lives here.
|
|
Apache-2.0, Mozilla Foundation. */
|
|
.textLayer{
|
|
color-scheme:only light;
|
|
|
|
position:absolute;
|
|
text-align:initial;
|
|
inset:0;
|
|
overflow:clip;
|
|
opacity:1;
|
|
line-height:1;
|
|
letter-spacing:normal;
|
|
word-spacing:normal;
|
|
-webkit-text-size-adjust:none;
|
|
-moz-text-size-adjust:none;
|
|
text-size-adjust:none;
|
|
forced-color-adjust:none;
|
|
transform-origin:0 0;
|
|
caret-color:CanvasText;
|
|
z-index:0;
|
|
|
|
&.highlighting{
|
|
touch-action:none;
|
|
}
|
|
|
|
:is(span, br){
|
|
color:transparent;
|
|
position:absolute;
|
|
white-space:pre;
|
|
cursor:text;
|
|
transform-origin:0% 0%;
|
|
-webkit-user-select:text;
|
|
-moz-user-select:text;
|
|
user-select:text;
|
|
}
|
|
--min-font-size:1;
|
|
--text-scale-factor:calc(var(--total-scale-factor) * var(--min-font-size));
|
|
--min-font-size-inv:calc(1 / var(--min-font-size));
|
|
|
|
> :not(.markedContent),
|
|
.markedContent span:not(.markedContent){
|
|
z-index:1;
|
|
|
|
--font-height:0;
|
|
font-size:calc(var(--text-scale-factor) * var(--font-height));
|
|
|
|
--scale-x:1;
|
|
--rotate:0deg;
|
|
transform:rotate(var(--rotate)) scaleX(var(--scale-x)) scale(var(--min-font-size-inv));
|
|
}
|
|
|
|
.markedContent{
|
|
display:contents;
|
|
}
|
|
|
|
span[role="img"]{
|
|
-webkit-user-select:none;
|
|
-moz-user-select:none;
|
|
user-select:none;
|
|
cursor:default;
|
|
}
|
|
|
|
.highlight{
|
|
--highlight-bg-color:rgb(180 0 170 / 0.25);
|
|
--highlight-selected-bg-color:rgb(0 100 0 / 0.25);
|
|
--highlight-backdrop-filter:none;
|
|
--highlight-selected-backdrop-filter:none;
|
|
|
|
@media screen and (forced-colors: active){
|
|
--highlight-bg-color:transparent;
|
|
--highlight-selected-bg-color:transparent;
|
|
--highlight-backdrop-filter:var(--hcm-highlight-filter);
|
|
--highlight-selected-backdrop-filter:var(
|
|
--hcm-highlight-selected-filter
|
|
);
|
|
}
|
|
|
|
margin:-1px;
|
|
padding:1px;
|
|
background-color:var(--highlight-bg-color);
|
|
backdrop-filter:var(--highlight-backdrop-filter);
|
|
border-radius:4px;
|
|
|
|
&.appended{
|
|
position:initial;
|
|
}
|
|
|
|
&.begin{
|
|
border-radius:4px 0 0 4px;
|
|
}
|
|
|
|
&.end{
|
|
border-radius:0 4px 4px 0;
|
|
}
|
|
|
|
&.middle{
|
|
border-radius:0;
|
|
}
|
|
|
|
&.selected{
|
|
background-color:var(--highlight-selected-bg-color);
|
|
backdrop-filter:var(--highlight-selected-backdrop-filter);
|
|
scroll-margin-top:50px;
|
|
}
|
|
}
|
|
|
|
::-moz-selection{
|
|
background:rgba(0 0 255 / 0.25);
|
|
background:color-mix(in srgb, AccentColor, transparent 50%);
|
|
color:transparent;
|
|
}
|
|
|
|
::selection{
|
|
background:rgba(0 0 255 / 0.25);
|
|
background:color-mix(in srgb, AccentColor, transparent 50%);
|
|
color:transparent;
|
|
}
|
|
|
|
&.selectionRendering{
|
|
::-moz-selection{
|
|
background:transparent;
|
|
color:transparent;
|
|
}
|
|
::selection{
|
|
background:transparent;
|
|
color:transparent;
|
|
}
|
|
}
|
|
br::-moz-selection{
|
|
background:transparent;
|
|
}
|
|
br::selection{
|
|
background:transparent;
|
|
}
|
|
|
|
.endOfContent{
|
|
display:block;
|
|
position:absolute;
|
|
inset:100% 0 0;
|
|
z-index:0;
|
|
cursor:default;
|
|
-webkit-user-select:none;
|
|
-moz-user-select:none;
|
|
user-select:none;
|
|
}
|
|
|
|
&.selecting .endOfContent{
|
|
top:0;
|
|
}
|
|
} |