Files
Skald-Circle/web/vendor/pdf-text-layer.css
T
Daniele 71e1a26b08
Nightly Build / build (push) Successful in 7m59s
fix(ui): render PDFs with pdf.js instead of an iframe
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.
2026-08-08 17:16:15 +01:00

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;
}
}