PDF Viewer Page Position Persistence#
Page position persistence in the Stirling-PDF viewer has two distinct scopes:
-
Within-session — the viewer tracks
currentPagein real time through theScrollAPIBridge→ViewerContextpipeline. This state is consumed by features like the inline bookmark form (to default the target page to where the user is currently reading) and is the mechanism implicated in the bookmark-reset bug . -
Cross-session — persisting the last-read page across application restarts is not yet implemented. It is an open feature request .
Within-Session Scroll State Tracking#
The scroll state pipeline has two layers:
ScrollAPIBridge.tsx is a renderless React component that bridges the @embedpdf/plugin-scroll plugin to the shared ViewerContext. It :
- Calls
useScroll(documentId)to get live{ currentPage, totalPages }from the EmbedPDF scroll plugin. - Extracts primitive values (
currentPage,totalPages) from the scroll state object to avoid stale dependency references. - Calls
triggerImmediateScrollUpdate(currentPage, totalPages)for responsive UI (bypasses React re-render cycle). - Calls
registerBridge("scroll", { state, api })to publish the state intoViewerContext's bridge registry. - Cleans up by calling
registerBridge("scroll", null)on unmount. - Only renders once
activeDocumentIdis set anduseDocumentReady()returns true; this prevents stale registrations during document transitions.
ViewerContext.tsx is the single source of truth for all viewer state . Two scroll-relevant APIs it exposes:
getScrollState()— synchronously readsbridgeRefs.current.scroll?.state, defaulting to{ currentPage: 1, totalPages: 0 }when no bridge is registered.registerImmediateScrollUpdate(callback)— registers a callback that fires synchronously on each scroll event, allowing components to read the latest page without waiting for a React render cycle.
Bookmark Operations and Position Preservation#
The Bookmark-Reset Bug (Issue #6917)#
Adding a bookmark from any page other than page 1 causes the viewer to jump back to page 1 . This was reported in v2.14.1 on both the web and Windows desktop builds.
The root cause is a race condition in the file-swap lifecycle: adding a bookmark via the inline form in BookmarkSidebar.tsx calls fileActions.consumeFiles(...), which replaces the active file and remounts the viewer. During the remount window, the scroll bridge unregisters. If activeFileId is not explicitly updated post-swap, the viewer falls back to activeFileIndex = 0 and the scroll bridge re-registers at page 1.
PR #6552 fix: After consumeFiles returns outputFileIds, the sidebar immediately calls setActiveFileId(outputFileIds[0]) to steer the viewer to the new file before the bridge race can produce a wrong position .
How the Bookmark Form Reads Current Page#
When the user clicks "Add bookmark", handleOpenAddBookmark in BookmarkSidebar.tsx reads :
const currentPage = getScrollState?.()?.currentPage ?? 1;
setNewBookmarkPage(currentPage);
This defaults the bookmark's target page to wherever the user currently is, matching the behavior of Acrobat and Foxit.
Bookmark Bridge Retry Logic#
Both the existing inline retry loop in BookmarkSidebar.tsx and the shared fetchWithReadyRetry utility introduced in PR #6837 handle the window where the bookmark bridge is null during document transitions . The key distinction: null from the bridge means "not ready yet" (retry), while [] means "document open, no bookmarks" (cache as success). Without this distinction, the sidebar cached an empty success and never refetched after the file swap.
Cross-Session Persistence (Feature Request)#
Remembering the last-read page across application restarts is not yet implemented . On every fresh load, the viewer defaults to page 1.
Issue #5520 tracks the feature request. The issue author proposes :
- Storage: SQLite or a JSON file mapping file identifier →
{ last_page, last_accessed }. - File identification: SHA-256 hash of the first few KB (handles renames/moves), falling back to absolute path.
- Write strategy: debounced updates (2–3 seconds on a page) to avoid excessive writes during rapid navigation.
- Restore logic: query storage on PDF open; navigate to the saved page if valid, otherwise default to page 1.
- UX controls: opt-in preference toggle ("Remember last page read"), optional "Go to first page" button, and periodic cleanup of stale entries (e.g., files not accessed in 90 days).
The bookmark-reset bug (#6917) reporter noted that a unified scroll-state persistence solution could address both issues .
Key Files and References#
| File | Role |
|---|---|
frontend/editor/src/core/components/viewer/ScrollAPIBridge.tsx | Connects EmbedPDF scroll plugin → ViewerContext bridge registry |
frontend/editor/src/core/contexts/ViewerContext.tsx | Exposes getScrollState(), registerImmediateScrollUpdate(), bridge registry |
frontend/editor/src/core/components/viewer/BookmarkSidebar.tsx | Reads getScrollState to pre-fill new bookmark page; manages file-swap race via setActiveFileId |
Issues & PRs:
- Issue #6917 — Bug: bookmark add resets page to 1
- Issue #5520 — Feature: remember last page read across restarts
- PR #6552 — Inline bookmark form +
setActiveFileIdpost-consumeFiles fix - PR #6837 —
fetchWithReadyRetryfor bookmark/attachment bridge null handling