📝 Documentation of pdf view customisation, the alfviewer

This project was initiated because I was not happy with how my PDF reader worked. I decided to make some customizations on top of PDFjs (open source, the Firefox pdf-viewer). I wanted to be able to integrate views of pdfs containing images into html pages and show a pdf page one at a time and also control how it looked on my iPhone..

A sample of this implementation can be viewed at Gallery Docs Presentation or a bit more dynamic version at Photo Tips

Custom PDF.js alfviewer Documentation

Custom PDF.js Viewer Documentation

1. Overview

This project was initiated because I was not happy with how my PDF reader worked. I decided to make some customizations on top of PDFjs (open source, the Firefox pdf-viewer). I wanted to be able to integrate views of pdfs containing images into html pages and show one pdf page at a time nicer and also control how it looked on my iPhone..

This documentation describes the architecture and customization of a custom PDF.js viewer with collapsible sections, syntax highlighting, and live search.

2. File Structure

project/
├── index.html
└── pdfjs/
    ├── alfviewer/alfviewer.js
    └── alfviewer/alfviewer.css
└── pdfjs/
    ├── pdf.js
    └── pdf.worker.js
      
3. PDF Loading Logic

The viewer uses PDF.js to load and render PDF documents. The main entry point is the loadPDF(url) function, which initializes the PDF document and renders the first page.

4. JavaScript Engine & Internal Logic

4.1 Overview

The JavaScript engine coordinates PDF loading, rendering, scaling, and UI updates. The core logic is organized into a set of key functions that each have a clear responsibility: loadPDF(url), renderPage(), calculateScale(), resetZoom(), fitPage(), updatePageIndicator(), and debugOverlay(message).

4.2 loadPDF(url)

Purpose

Load a PDF document from a given URL using PDF.js and prepare the viewer for rendering.

What it does

  • Fetches and parses the PDF via pdfjsLib.getDocument(url).
  • Stores the PDF object in pdfDoc.
  • Sets currentPage = 1.
  • Calls renderPage() to draw the first page.

When it runs

  • On initial page load.
  • Whenever switching to a different PDF.

Customizable parts

  • Add loading spinners.
  • Add error messages.
  • Preload multiple PDFs.

let pdfDoc = null;
let currentPage = 1;

async function loadPDF(url) {
    try {
        pdfDoc = await pdfjsLib.getDocument(url).promise;
        currentPage = 1;
        renderPage();
    } catch (err) {
        console.error("Error loading PDF:", err);
        debugOverlay("Failed to load PDF.");
    }
}
      

4.3 renderPage()

Purpose

Render the current page of the loaded PDF into the canvas element.

What it does

  • Retrieves the page object.
  • Calculates scale via calculateScale().
  • Creates a viewport.
  • Resizes the canvas.
  • Renders the page.
  • Updates the page indicator.

const canvas = document.getElementById("pdf-canvas");
const ctx = canvas.getContext("2d");

async function renderPage() {
    if (!pdfDoc) return;

    const page = await pdfDoc.getPage(currentPage);
    const scale = calculateScale(page);
    const viewport = page.getViewport({ scale });

    canvas.width = viewport.width;
    canvas.height = viewport.height;

    await page.render({
        canvasContext: ctx,
        viewport: viewport
    }).promise;

    updatePageIndicator();
}
      

4.4 calculateScale()

Purpose

Determine the appropriate zoom level based on device type and orientation.


function calculateScale(page) {
    const container = document.getElementById("viewerContainer");
    const viewport = page.getViewport({ scale: 1 });
    const containerWidth = container.clientWidth;

    const isMobile = window.innerWidth < 768;
    const isLandscape = window.innerWidth > window.innerHeight;

    if (!isMobile) return containerWidth / viewport.width;
    if (!isLandscape) return containerWidth / viewport.width;

    return 0.65;
}
      
4.5–4.8 Additional JS helpers

4.5 resetZoom()

Restores the viewer to the default scale.


function resetZoom() {
    renderPage();
}
      

4.6 fitPage()

Forces the viewer into “fit entire page” mode.


let forceFitPage = false;

function fitPage() {
    forceFitPage = true;
    renderPage();
}
      

4.7 updatePageIndicator()


function updatePageIndicator() {
    const indicator = document.getElementById("pageIndicator");
    if (!indicator || !pdfDoc) return;
    indicator.textContent = `Page ${currentPage} of ${pdfDoc.numPages}`;
}
      

4.8 debugOverlay(message)


function debugOverlay(message) {
    let overlay = document.getElementById("debugOverlay");
    if (!overlay) {
        overlay = document.createElement("div");
        overlay.id = "debugOverlay";
        overlay.style.position = "fixed";
        overlay.style.bottom = "10px";
        overlay.style.right = "10px";
        overlay.style.background = "rgba(0,0,0,0.7)";
        overlay.style.color = "#fff";
        overlay.style.padding = "8px 12px";
        overlay.style.fontSize = "12px";
        overlay.style.borderRadius = "4px";
        overlay.style.zIndex = "9999";
        document.body.appendChild(overlay);
    }
    overlay.textContent = message;
}
      
5. CSS Architecture

The CSS defines layout, canvas behavior, floating buttons, and optional debug overlay.

  • #viewerContainer – main wrapper
  • #pageWrapper – centers the canvas
  • #pdf-canvas – rendered PDF page
  • .floatBtn – floating control buttons
  • #pageIndicator – page number overlay
  • #debugOverlay – optional debug layer
6. Embedding the Viewer

<div id="viewerContainer">
  <div id="pageWrapper">
    <canvas id="pdf-canvas"></canvas>
  </div>

  <button id="resetZoomBtn" class="floatBtn">🔄</button>
  <button id="fitPageBtn" class="floatBtn">🗎</button>

  <div id="pageIndicator"></div>
</div>
      
7. Customization Options

The viewer is intentionally designed to be modular and easy to adapt. This section explains what you can customize and how to do it in practical terms.

7.1 Default PDF

If the page is opened without a ?pdf= parameter, the viewer loads a fallback file. You can change this default in your main script.


// Example: change default PDF
const defaultPDF = "/pdfs/my-startup-file.pdf";

// If no ?pdf= parameter is found:
const urlParams = new URLSearchParams(window.location.search);
let pdfFile = urlParams.get("pdf") || defaultPDF;
loadPDF(pdfFile);
  

Where to customize: top of alfviewer.js.

7.2 Scaling Behavior

The scaling logic is controlled by calculateScale(). You can adjust how the viewer behaves on desktop, mobile portrait, and mobile landscape.


// Example: tweak mobile landscape scale
if (isMobile && isLandscape) {
    return 0.75; // previously 0.65
}
  

Where to customize: inside calculateScale().

  • Fit width → return containerWidth / viewport.width
  • Fit height → return containerHeight / viewport.height
  • Fixed zoom → return 1.0 (or any number)

7.3 Buttons

The floating buttons (reset zoom, fit page, etc.) are simple HTML elements you can add, remove, or restyle.




  

// Add behavior in alfviewer.js
document.getElementById("nextPageBtn").addEventListener("click", () => {
    if (currentPage < pdfDoc.numPages) {
        currentPage++;
        renderPage();
    }
});
  

Where to customize: HTML for buttons, JS for behavior.

7.4 Debug Overlay

The debug overlay is optional and now split into a separate include to avoid flashing debug UI in production. Use the debug include only during development.

How the new debug workflow works

  • Debug include file — create a single file (for example /html/includes/alfviewer-debug.shtml) that contains the debug HTML, small float, big panel, and its script. When you include this file in flat.shtml the debug UI is available.
  • Include marker — the debug include should set a simple marker so the viewer knows the include is present:
    <script>window.ALF_VIEWER_DEBUG_INCLUDED = true;</script>
    The viewer checks this marker before creating or showing the small debug float to prevent flash-of-debug when the include is not present.
  • Config and URL — the viewer still respects window.ALF_VIEWER_CONFIG.debug and the ?debug URL parameter for quick testing. Priority order used by the viewer:
    1. ALF_VIEWER_DEBUG_INCLUDED (include present)
    2. ?debug URL parameter
    3. ALF_VIEWER_CONFIG.debug
  • CSS guard — to avoid a brief flash while scripts load, the docs recommend adding this CSS so the small float is hidden by default and only shown when JS explicitly adds the alf-visible class:
    #debugOverlay { display: none !important; visibility: hidden !important; pointer-events: none !important; }
    #debugOverlay.alf-visible { display: block !important; visibility: visible !important; pointer-events: auto !important; }
  • Console helpers — for quick runtime toggles during development you can use:
    window.toggleAlfDebug = function(force) { window.ALF_VIEWER_CONFIG = window.ALF_VIEWER_CONFIG || {}; if (typeof force === 'boolean') window.ALF_VIEWER_CONFIG.debug = force; else window.ALF_VIEWER_CONFIG.debug = !window.ALF_VIEWER_CONFIG.debug; window.dispatchEvent(new Event('resize')); };
    and an immediate alias:
    window.toggleAlfDebugImmediate = function(){ /* toggles overlays immediately */ };

Where to include

Add the SSI include in flat.shtml only on development/staging pages:

<!--#include virtual="/pdfjs/alfviewer/alfviewer-debug.shtml" -->
Remove or comment out that include for production to ensure no debug UI or scripts are present.

Where to customize: inside the debug include file and in alfviewer.js if you want different behavior.

Notes: The viewer code was updated to check for the include marker before creating the small float. If you prefer the URL param to always override the config, the viewer can be adjusted so ?debug takes precedence.

7.5 Page Indicator

The page indicator is a small overlay showing the current page number. You can change its position, style, or format.




  

// Change format
indicator.textContent = `Page ${currentPage} / ${pdfDoc.numPages}`;
  

Where to customize: CSS for position, JS for text format.

7.6 Viewer Size

The viewer container can be resized to fit your layout. For example, you can limit the maximum width or make it full-screen.


/* Example: limit viewer width */
#viewerContainer {
  max-width: 900px;
  margin: auto;
}

/* Example: full-screen viewer */
#viewerContainer {
  width: 100vw;
  height: 100vh;
}
  

Where to customize: viewer.css or inline styles.

7.7 Keyboard Shortcuts (Optional)

You can add keyboard shortcuts for navigation, zoom, or debugging.


// Example: press "D" to toggle debug overlay
document.addEventListener("keydown", (e) => {
    if (e.key === "d") {
        debugOverlay("Debug toggled at " + new Date().toLocaleTimeString());
    }
});
  

Where to customize: anywhere in alfviewer.js.

7.8 Fit Modes (Advanced)

You can add multiple fit modes: fit width, fit height, fit page, or custom zoom. These modes give users more control over how the PDF is displayed.

7.8.1 Fit Width


function fitWidth() {
    const container = document.getElementById("viewerContainer");
    pdfDoc.getPage(currentPage).then(page => {
        const viewport = page.getViewport({ scale: 1 });
        const scale = container.clientWidth / viewport.width;
        customScale = scale;
        renderPage();
    });
}
  

7.8.2 Fit Height


// Example: fit height mode
function fitHeight() {
    const container = document.getElementById("viewerContainer");
    const page = pdfDoc.getPage(currentPage);
    page.then(p => {
        const viewport = p.getViewport({ scale: 1 });
        const scale = container.clientHeight / viewport.height;
        customScale = scale;
        renderPage();
    });
}
  

7.8.3 Fit Page (entire page visible)


function fitPage() {
    const container = document.getElementById("viewerContainer");
    pdfDoc.getPage(currentPage).then(page => {
        const viewport = page.getViewport({ scale: 1 });

        const scaleW = container.clientWidth / viewport.width;
        const scaleH = container.clientHeight / viewport.height;

        customScale = Math.min(scaleW, scaleH);
        renderPage();
    });
}
  

7.8.4 Custom Zoom


function setZoom(level) {
    customScale = level; // e.g. 0.5, 1.0, 2.0
    renderPage();
}
  

Where to customize: add new functions + buttons for each fit mode.

7.9 Multi‑PDF Support

You can allow users to switch between multiple PDFs without reloading the page. This is useful for document libraries, manuals, or multi-chapter systems.


// Example: load a new PDF dynamically
function switchPDF(path) {
    loadPDF(path);
}
  

Where to customize: add UI elements (dropdown, list, buttons, etc.).


7.10 Styling Themes (Light / Dark Mode)

The viewer can support multiple visual themes. The simplest approach is to toggle a dark-mode class on the <body> element.


// Toggle dark mode
function toggleDarkMode() {
    document.body.classList.toggle("dark-mode");
}
  

/* Light mode (default) */
body {
  background: #f9f9f9;
  color: #333;
}

/* Dark mode */
body.dark-mode {
  background: #1e1e1e;
  color: #e0e0e0;
}

body.dark-mode pre {
  background: #2a2a2a;
  border-left-color: #555;
}
  

Where to customize: add a button or keyboard shortcut to call toggleDarkMode().


7.11 Embedding Inside CMS / iframe

If you embed the viewer inside a CMS (WordPress, Joomla, Drupal) or an <iframe>, you may need to adjust sizing and communication.

7.11.1 Responsive iframe


<iframe
  src="/viewer/pdfviewer-docs.html?pdf=/pdfs/manual.pdf"
  style="width:100%; height:90vh; border:none;"
></iframe>
  

7.11.2 Auto-resize iframe (optional)


// Inside the viewer page:
window.addEventListener("load", () => {
    const height = document.body.scrollHeight;
    parent.postMessage({ viewerHeight: height }, "*");
});
  

// On the parent page:
window.addEventListener("message", (e) => {
    if (e.data.viewerHeight) {
        document.getElementById("myIframe").style.height = e.data.viewerHeight + "px";
    }
});
  

Where to customize: parent page + viewer page.


7.12 Performance Tuning & Caching

For large PDFs or mobile devices, performance can be improved with caching and rendering strategies.

7.12.1 Page caching

Store rendered pages in memory to avoid re-rendering when navigating back.


const pageCache = {};

async function renderPage() {
    if (pageCache[currentPage]) {
        drawFromCache(pageCache[currentPage]);
        updatePageIndicator();
        return;
    }

    const page = await pdfDoc.getPage(currentPage);
    const scale = calculateScale(page);
    const viewport = page.getViewport({ scale });

    const canvas = document.createElement("canvas");
    canvas.width = viewport.width;
    canvas.height = viewport.height;

    await page.render({
        canvasContext: canvas.getContext("2d"),
        viewport
    }).promise;

    pageCache[currentPage] = canvas;
    drawFromCache(canvas);
    updatePageIndicator();
}

function drawFromCache(cachedCanvas) {
    const mainCanvas = document.getElementById("pdf-canvas");
    mainCanvas.width = cachedCanvas.width;
    mainCanvas.height = cachedCanvas.height;
    mainCanvas.getContext("2d").drawImage(cachedCanvas, 0, 0);
}
  

7.12.2 Lazy rendering

If you add page thumbnails or multi-page views, render only what is visible.


// Example: only render thumbnails when scrolled into view
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      renderThumbnail(entry.target.dataset.pageNumber);
    }
  });
});
  

7.12.3 Worker optimization

PDF.js uses a worker thread. You can host pdf.worker.js on a CDN or local path for faster loading.


pdfjsLib.GlobalWorkerOptions.workerSrc = "/pdfjs/pdf.worker.js";
  

Where to customize: top of alfviewer.js.

7.13 Mobile & Small‑Screen Behavior

The viewer includes several optimizations for phones and small tablets. These ensure that PDFs remain readable and the interface stays usable even on very limited screen space.

7.13.1 Automatic Scaling Rules

The function calculateScale() contains special logic for mobile devices. It detects screen size and orientation, then adjusts the zoom level accordingly.


const isMobile = window.innerWidth < 768;
const isLandscape = window.innerWidth > window.innerHeight;

if (!isMobile) {
    return containerWidth / viewport.width; // Desktop → fit width
}

if (!isLandscape) {
    return containerWidth / viewport.width; // Mobile portrait → fit width
}

return 0.65; // Mobile landscape → reduced zoom for readability
  
  • Desktop: Fit width
  • Mobile portrait: Fit width (max readability)
  • Mobile landscape: Reduced zoom (prevents horizontal scrolling)

7.13.2 Touch‑Friendly Buttons

Floating buttons are intentionally large and spaced apart to support touch interaction. You can adjust their size for mobile users.


/* Larger buttons on small screens */
@media (max-width: 600px) {
  .floatBtn {
    width: 48px;
    height: 48px;
    font-size: 1.4rem;
  }
}
  

7.13.3 Canvas Resizing on Orientation Change

When a user rotates their device, the viewer automatically re-renders the page to match the new width/height ratio.


window.addEventListener("orientationchange", () => {
    setTimeout(() => renderPage(), 200);
});
  

The small delay ensures the browser has updated the viewport dimensions before rendering.

7.13.4 Avoiding Horizontal Scrolling

On small screens, horizontal scrolling makes PDFs difficult to read. The viewer prevents this by:

  • Fitting width or reducing scale
  • Centering the canvas inside #pageWrapper
  • Using overflow-x: hidden on the container

#viewerContainer {
  overflow-x: hidden;
}
  

7.13.5 Mobile‑Optimized Page Indicator

The page indicator is positioned so it doesn’t overlap the content on small screens.


@media (max-width: 600px) {
  #pageIndicator {
    bottom: 10px;
    right: 10px;
    font-size: 0.9rem;
    padding: 4px 8px;
  }
}
  

7.13.6 Optional: Hide Non‑Essential UI on Mobile

You can hide or simplify UI elements on small screens to maximize reading space.


@media (max-width: 600px) {
  #debugOverlay {
    display: none;
  }
}
  

Where to customize: scaling logic, CSS media queries, and optional UI visibility rules.

8. How It Works – Summary
  1. Page loads
  2. Default PDF ensured
  3. loadPDF() loads document
  4. renderPage() draws first page
  5. calculateScale() determines zoom
  6. User can reset or fit page
  7. Page indicator updates
  8. Debug overlay optional
9. Diagrams

9.1 System Architecture


+---------------------------+
|        Web Page          |
+------------+-------------+
             |
             v
+----------------------------------+
|   alfviewer.js Engine       |
+---------------+----------------+
             |
             v
+---------------------------+
|          PDF.js          |
+------------+-------------+
             |
             v
+---------------------------+
|        Canvas            |
+---------------------------+
      

9.2 Rendering Flow


Page Loads
   |
Check ?pdf=
   |
loadPDF()
   |
renderPage()
   |
calculateScale()
   |
Draw to Canvas
      

9.3 Scaling Logic


Desktop → Fit Page
Mobile Portrait → Fit Width
Mobile Landscape → 0.65
      
10. Fixes and changelog

Summary of recent fixes and rationale

During the last development session we split the debug UI out of the main viewer and made the viewer defensive about creating debug UI. The goals were:

  • Prevent a brief flash of the small debug float on page load when debug is not intended.
  • Keep production pages free of debug HTML/CSS/JS unless explicitly included.
  • Preserve developer convenience via a single include and console helpers for quick toggling.
  • Keep the renderer DPR-aware and idempotent while avoiding layout glitches during initial load and resize.

What changed (high level)

  • Debug UI moved to an include — a single file (e.g., alfviewer-debug.shtml) now contains the small float, the large debug panel, and the debug wiring. Include it only on development pages.
  • Include marker — the debug include sets window.ALF_VIEWER_DEBUG_INCLUDED = true. The viewer checks this marker before creating or showing the small float.
  • Viewer guardalfviewer.js was updated to only create or reveal debug UI when the include marker, the ?debug URL param, or ALF_VIEWER_CONFIG.debug explicitly request it.
  • CSS guard — the docs recommend hiding #debugOverlay by default and only showing it when JS adds .alf-visible, preventing flash-of-debug.
  • Console helpers — small runtime helpers (e.g., toggleAlfDebug()) were added for quick on/off toggling during development.
  • Render and layout tweaks — the renderer remains DPR-aware and idempotent; layout and centering logic were adjusted to reduce visual glitches on initial load and on resize/orientation changes.

Files touched or recommended

  • alfviewer-debug.shtml — new include file (development only). Should contain the debug HTML and script and set window.ALF_VIEWER_DEBUG_INCLUDED = true.
  • alfviewer.js — updated to check the debug include marker and to only update debug UI when explicitly enabled; also small rendering and update improvements.
  • alfviewer.css — add the CSS guard for #debugOverlay (shown at top of this doc) to prevent flashes.
  • flat.shtml — include or remove the debug include as needed; keep ALF_VIEWER_CONFIG.debug set to false by default in production.

Quick checklist before publishing

  1. Remove or comment out the SSI include for alfviewer-debug.shtml from production pages.
  2. Ensure ALF_VIEWER_CONFIG.debug is false in production.
  3. Hard reload pages after changes to avoid cached scripts/styles.
  4. Verify small float does not appear when the include is absent and debug is disabled.

Example snippets

Include marker (place at top of debug include):

<script>window.ALF_VIEWER_DEBUG_INCLUDED = true;</script>

Recommended CSS guard (already included at top of this doc):

#debugOverlay { display: none !important; visibility: hidden !important; pointer-events: none !important; }
#debugOverlay.alf-visible { display: block !important; visibility: visible !important; pointer-events: auto !important; }

Console helper (example):

window.toggleAlfDebug = function(force) {
  window.ALF_VIEWER_CONFIG = window.ALF_VIEWER_CONFIG || {};
  if (typeof force === 'boolean') window.ALF_VIEWER_CONFIG.debug = force;
  else window.ALF_VIEWER_CONFIG.debug = !window.ALF_VIEWER_CONFIG.debug;
  window.dispatchEvent(new Event('resize'));
};

Documentation maintained by Alf — Updated 2026