New Features
- Right-click context menu for internal PDF links: Right-clicking any internal link now shows a menu with:
- Open in New Tab — Opens the same document in a new tab and navigates to the link target.
- Open in Preview — Shows the link target in the floating preview overlay (same as the configured preview mouse action).
- Open as Portal — Creates a portal split to the link target (same as the configured Ctrl+click action).
- Open in Split → Vertical / Horizontal — Opens a vertical or horizontal split unconditionally, ignoring the portal split-direction configuration.
- Copy Link Address — Copies the link destination string to the clipboard (renamed from "Copy Link Location").
- Note: External links will only show "Copy Link Address". The menu is implemented in
BrowseLinkItem::contextMenuEvent(); each action emits a signal that bubbles throughDocumentViewtoLektra, reusing the existing preview and portal infrastructure where possible.
Improvements
-
Eliminate redundant work in the render cycle:
-
updateSceneRect()was being called unconditionally at the end ofrenderPages(), even when the zoom-bake block had already called it in the same cycle. This caused a doublesetSceneRect()on every zoom event. It is now skipped when zoom has just been baked (except in thumbnail mode, where item bounds contribute to the scene rect). -
getPreloadPages()was re-enteringgetVisiblePages()internally, even though the caller already held the result. The signature has been changed to accept the visible-page set as a parameter, eliminating the redundant cache lookup on every render cycle. -
cachePageStride()was allocating aQFontandQFontMetricsFon every call in thumbnail mode to compute the label row height, even though the value is constant for a given configuration. The result is now cached inm_thumbnail_label_heightand computed only once. -
Reduce default memory usage significantly: Three sources of excess allocation have been eliminated:
-
MuPDF internal store reduced from
FZ_STORE_DEFAULT(256 MB) to 64 MB. The store caches decoded embedded images, fonts, and glyph bitmaps; the old cap allowed it to silently consume the majority of the process RSS on image-heavy documents. The limit is now configurable viabehavior.mupdf_store_size(integer, MB). -
Alpha channel removed from rendered pixmaps:
fz_new_pixmap_with_bboxwas called withalpha=1, producing 4-component RGBA pixmaps even though the page background is always cleared to opaque white and the alpha plane is never used. Changed toalpha=0; rendered pages are now stored asFormat_RGB888(3 bytes/pixel) instead ofFormat_RGBA8888(4 bytes/pixel), yielding a 25% reduction per cached and displayed page image. -
OpenGL MSAA disabled: The OpenGL viewport was configured with 4× MSAA (
format.setSamples(4)), which multiplies the GPU framebuffer size (color, depth, stencil) by four. On integrated-GPU systems, this memory is carved from system RAM and shows up directly in process RSS (~60–100 MB at 1080p). MSAA brings no quality benefit for a document viewer because MuPDF already antialiases text and images at the CPU level. Samples are now always set to 0. -
OpenGL
CacheBackgroundremoved:QGraphicsView::CacheBackgroundallocated a redundant full-viewport pixmap for what is typically a solid-color scene background. Replaced withCacheNone. -
Change default rendering backend from
Auto(OpenGL when available) toRaster: The OpenGL backend adds ~150–185 MB of driver and framebuffer overhead with no meaningful quality or performance benefit for document viewing. OpenGL remains available viarendering.backend = "opengl"in the configuration.
Bug Fixes
- Fix pages rendering slanted after zoom when using RGB (non-alpha) pixmaps: The two
QImageconstruction sites in the render pipeline were usingQImage(w, h, fmt)+memcpy(image.bits(), samples, stride * height). ForFormat_RGB888(3 bytes/pixel), Qt pads each scanline to the next 4-byte boundary. When the rendered page width was not divisible by 4, the bulkmemcpywrote rows without the padding, shifting every subsequent row and producing a diagonal slant. Fixed by using the stride-aware constructorQImage(samples, w, h, stride, fmt).copy()at both sites, which lets Qt handle the scanline alignment internally. - Fix redundant page-image copy on every render: The render callback passed
const QImage &imagedown throughrenderPageFromImage→createAndAddPageItem→setImage(const QImage &), triggering a full pixel-buffer copy (~3–10 MB per page). Since thePageRenderResultis a local value in the callback, the image is now moved withstd::moveall the way intoGraphicsImageItem, eliminating the copy entirely. - Fix pages appearing blank during fast scrolling: The scroll handlers (
handleVScrollValueChanged/handleHScrollValueChanged) previously only restarted the 66ms debounce timer on each scroll event, meaningrenderPages()—and therefore any render requests for newly visible pages—would not fire until scrolling stopped. Visible pages are now queued for rendering immediately on every scroll event viarequestPageRender(); the debounce timer still fires afterward to handle cleanup (pruning stale renders, removing off-screen items, updating preload pages). - Fix viewport jumping to the wrong page during Ctrl+scroll zoom in multi-page continuous layout: The zoom path applies a GPU view-transform (
m_gview->scale()) immediately for O(1) visual feedback, then defers the expensiverepositionPages()bake to a 66ms debounce timer. During the bake,resetTransform()followed byupdateSceneRect()caused Qt to auto-clamp scrollbar values and emitvalueChangedonm_vscroll/m_hscrollbeforecenterOn()had a chance to restore the correct viewport position. Becausem_gscene->blockSignals(true)only suppressesQGraphicsScenesignals (not scrollbar signals), the scroll handlers fired with an incorrect position, updated the current-page counter, and queued renders for the wrong pages. Fixed by also callingm_vscroll->blockSignals(true)/m_hscroll->blockSignals(true)around the entire bake critical section and releasing them aftersetUpdatesEnabled(true)at the end of the merged suppression window. - Fix pages disappearing off-screen after zoom in multi-page layout: After the zoom bake,
centerOn()(andGotoPage()for the gap fallback) were called while scrollbar signals were still blocked. Qt routes scroll position updates throughsetValue()→valueChanged→scrollContentsBy(), which physically moves the viewport. With signals blocked, that chain was severed; the scrollbar stored the correct target value, but the viewport never moved. This resulted in a blank view after every zoom bake—the pages were correctly positioned in the scene, but the viewport was pointing at empty space. Scrolling manually would triggervalueChanged, snapping the viewport to the stored value and revealing the pages. Fixed by unblockingm_vscroll/m_hscrollsignals immediately afterrepositionPages()and before thecenterOn()/GotoPage()call, keeping the block only for theresetTransform()+updateSceneRect()+repositionPages()critical section where spurious scroll events must be suppressed.