glyphline
Svelte

PDF pages

A pdf.js page component with the text layer and a glyphline overlay, in Svelte 5.

The same page as on the React side, as a Svelte 5 component. asyncText turns the pdf.js page into a source; once the text layer is rendered, the source also uses it as a fallback and selections work.

<script lang="ts">
  import type { PDFPageProxy } from "pdfjs-dist";
  import { pdfSource, type PdfTextSource } from "glyphline/pdf";
  import { asyncText, GlyphOverlay } from "glyphline/svelte";
  import "glyphline/style.css";
  import "glyphline/pdf.css";

  let { page, scale, sentence, word }: {
    page: PDFPageProxy; // from getDocument({ url, fontExtraProperties: true })
    scale: number;
    sentence: number;
    word: number;
  } = $props();

  let host: HTMLDivElement;
  let canvas: HTMLCanvasElement;
  let layer: HTMLDivElement;
  let layered = $state.raw<PdfTextSource | null>(null);

  const base = asyncText(async () => {
    const { OPS } = await import("pdfjs-dist");
    return pdfSource(page, { OPS });
  });
  const vp = $derived(page.getViewport({ scale }));

  $effect(() => {
    const src = base.source;
    if (!src) return;
    const { width, height } = vp;
    const dpr = devicePixelRatio;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    const ctx = canvas.getContext("2d")!;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    host.style.setProperty("--scale-factor", String(scale));
    host.style.setProperty("--total-scale-factor", String(scale));
    const render = page.render({ canvas, canvasContext: ctx, viewport: vp });
    let tl: import("pdfjs-dist").TextLayer | null = null;
    render.promise.then(async () => {
      const { TextLayer } = await import("pdfjs-dist");
      layer.replaceChildren();
      tl = new TextLayer({ textContentSource: await page.getTextContent(), container: layer, viewport: vp });
      await tl.render();
      layered = src.withTextLayer(tl.textDivs, layer, scale);
    }, () => {});
    return () => {
      render.cancel();
      tl?.cancel();
    };
  });
</script>

<div bind:this={host} style="position: relative; width: {vp.width}px; height: {vp.height}px">
  <canvas bind:this={canvas} style="position: absolute; inset: 0; width: 100%; height: 100%"></canvas>
  <div bind:this={layer} class="textLayer"></div>
  <GlyphOverlay source={layered ?? base.source} {sentence} {word} theme="light" />
</div>

theme="light" keeps the light colours on the white page in a dark app. The PDF guide explains the layers, fontExtraProperties and zoom.

Made by Lucas Piera