Skip to content

Commit e69dd4c

Browse files
committed
feat: hi-DPI rendering, floating palette clamping, and saver.js decomposition
- Render PDF and annotation canvases at native devicePixelRatio for sharp text on hi-DPI displays - Clamp floating tool palettes within viewport on window resize and app startup - Use actual palette element dimensions for boundary clamping (supports large icon mode) - Extract text-edits, watermarks, bookmarks, and utilities from saver.js into saver/ sub-modules
1 parent 4dbcfc0 commit e69dd4c

18 files changed

Lines changed: 837 additions & 735 deletions

File tree

open-pdf-studio/js/annotations/rendering.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -916,19 +916,21 @@ export function redrawAnnotations(lightweight = false) {
916916

917917
annotationCtx.clearRect(0, 0, annotationCanvas.width, annotationCanvas.height);
918918

919-
// Apply scale transformation for zooming
919+
// Apply scale transformation for zooming (includes hi-DPI factor)
920+
const dpr = window.devicePixelRatio || 1;
921+
const effectiveScale = scale * dpr;
920922
annotationCtx.save();
921-
annotationCtx.scale(scale, scale);
923+
annotationCtx.scale(effectiveScale, effectiveScale);
922924

923925
// Draw grid overlay if enabled
924926
if (state.preferences.showGrid) {
925-
drawGrid(annotationCtx, annotationCanvas.width / scale, annotationCanvas.height / scale);
927+
drawGrid(annotationCtx, annotationCanvas.width / effectiveScale, annotationCanvas.height / effectiveScale);
926928
}
927929

928930
const curPage = doc ? doc.currentPage : 1;
929931

930932
// Draw watermarks behind content
931-
renderWatermarksBehind(annotationCtx, curPage, annotationCanvas.width / scale, annotationCanvas.height / scale);
933+
renderWatermarksBehind(annotationCtx, curPage, annotationCanvas.width / effectiveScale, annotationCanvas.height / effectiveScale);
932934

933935
// Draw text edits (cover-and-replace) before annotations
934936
drawTextEdits(annotationCtx, curPage);
@@ -943,7 +945,7 @@ export function redrawAnnotations(lightweight = false) {
943945
annotationCtx.globalCompositeOperation = 'source-over';
944946

945947
// Draw watermarks in front of content
946-
renderWatermarksInFront(annotationCtx, curPage, annotationCanvas.width / scale, annotationCanvas.height / scale);
948+
renderWatermarksInFront(annotationCtx, curPage, annotationCanvas.width / effectiveScale, annotationCanvas.height / effectiveScale);
947949

948950
// Draw selection highlight and handles (use selectedAnnotations array as source of truth)
949951
const _renderDoc = getActiveDocument();
@@ -974,20 +976,22 @@ export function redrawAnnotations(lightweight = false) {
974976
}
975977

976978
// Render annotations for a specific page (continuous mode)
977-
export function renderAnnotationsForPage(ctx, pageNum, width, height) {
979+
export function renderAnnotationsForPage(ctx, pageNum, width, height, overrideDpr) {
978980
ctx.clearRect(0, 0, width, height);
979981

980982
// Read scale and annotations from the active document directly
981983
const doc = state.documents[state.activeDocumentIndex];
982984
const scale = doc ? doc.scale : 1;
983985
const annotations = doc ? doc.annotations : [];
984986

985-
// Apply scale transformation for zooming
987+
// Apply scale transformation for zooming (includes hi-DPI factor)
988+
const dpr = overrideDpr !== undefined ? overrideDpr : (window.devicePixelRatio || 1);
989+
const effectiveScale = scale * dpr;
986990
ctx.save();
987-
ctx.scale(scale, scale);
991+
ctx.scale(effectiveScale, effectiveScale);
988992

989993
// Draw watermarks behind content
990-
renderWatermarksBehind(ctx, pageNum, width / scale, height / scale);
994+
renderWatermarksBehind(ctx, pageNum, width / effectiveScale, height / effectiveScale);
991995

992996
// Draw text edits (cover-and-replace)
993997
drawTextEdits(ctx, pageNum);
@@ -998,7 +1002,7 @@ export function renderAnnotationsForPage(ctx, pageNum, width, height) {
9981002
});
9991003

10001004
// Draw watermarks in front of content
1001-
renderWatermarksInFront(ctx, pageNum, width / scale, height / scale);
1005+
renderWatermarksInFront(ctx, pageNum, width / effectiveScale, height / effectiveScale);
10021006

10031007
// Restore context
10041008
ctx.restore();

open-pdf-studio/js/annotations/stamps.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,9 @@ export async function placeOverrideStamp(x, y) {
130130
const canvas = document.getElementById('annotation-canvas') || document.getElementById('pdf-canvas');
131131
const doc = getActiveDocument();
132132
const stampScale = doc?.scale || 1.5;
133-
const pageW = canvas ? canvas.width / stampScale : 600;
134-
const pageH = canvas ? canvas.height / stampScale : 800;
133+
const dpr = window.devicePixelRatio || 1;
134+
const pageW = canvas ? canvas.width / (stampScale * dpr) : 600;
135+
const pageH = canvas ? canvas.height / (stampScale * dpr) : 800;
135136
const margin = overrides.stampPageMargin || 20;
136137
stampWidth = Math.round(pageW - margin * 2);
137138
stampHeight = Math.round(pageH - margin * 2);

open-pdf-studio/js/pdf/exporter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export async function renderPageOffscreen(pageNum, exportScale) {
8181
const savedScale = state.documents[state.activeDocumentIndex].scale;
8282
state.documents[state.activeDocumentIndex].scale = exportScale;
8383

84-
renderAnnotationsForPage(annCtx, pageNum, annCanvas.width, annCanvas.height);
84+
renderAnnotationsForPage(annCtx, pageNum, annCanvas.width, annCanvas.height, 1);
8585

8686
// Restore original scale
8787
state.documents[state.activeDocumentIndex].scale = savedScale;

open-pdf-studio/js/pdf/renderer.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ import { clearPdfVectorCache, prefetchPdfVectorGeometry } from '../tools/pdf-sna
1515
import { clearDetectionCache } from '../tools/pdf-element-detector.js';
1616
import { onPageRendered, clearHighlights } from '../search/find-bar.js';
1717

18+
// Hi-DPI support: render canvases at device pixel ratio for sharp text
19+
export function getCanvasDPR() { return window.devicePixelRatio || 1; }
20+
21+
function setupCanvasHiDPI(canvas, width, height) {
22+
const dpr = getCanvasDPR();
23+
canvas.width = Math.floor(width * dpr);
24+
canvas.height = Math.floor(height * dpr);
25+
canvas.style.width = Math.floor(width) + 'px';
26+
canvas.style.height = Math.floor(height) + 'px';
27+
}
28+
1829
// Track current render task to cancel if needed
1930
let currentRenderTask = null;
2031

@@ -55,11 +66,9 @@ export async function renderPage(pageNum) {
5566
const annotationCanvas = getAnnotationCanvas();
5667
if (!pdfCanvas || !annotationCanvas) return;
5768

58-
// Set canvas dimensions
59-
pdfCanvas.width = viewport.width;
60-
pdfCanvas.height = viewport.height;
61-
annotationCanvas.width = viewport.width;
62-
annotationCanvas.height = viewport.height;
69+
// Set canvas dimensions (hi-DPI: buffer at dpr, CSS at logical size)
70+
setupCanvasHiDPI(pdfCanvas, viewport.width, viewport.height);
71+
setupCanvasHiDPI(annotationCanvas, viewport.width, viewport.height);
6372

6473
// Set CSS scale variables for PDF.js text/annotation layers
6574
const container = document.getElementById('canvas-container');
@@ -70,9 +79,11 @@ export async function renderPage(pageNum) {
7079

7180
// Render PDF page
7281
const ctx = pdfCanvas.getContext('2d');
82+
const dpr = getCanvasDPR();
7383
const renderContext = {
7484
canvasContext: ctx,
7585
viewport: viewport,
86+
transform: dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : null,
7687
annotationMode: 0 // DISABLE - annotations are rendered by the app's overlay canvas
7788
};
7889

@@ -172,20 +183,18 @@ async function renderContinuousPage(pageNum) {
172183
canvasContainer.style.setProperty('--scale-factor', viewport.scale);
173184
canvasContainer.style.setProperty('--total-scale-factor', viewport.scale);
174185

175-
// Create PDF canvas
186+
// Create PDF canvas (hi-DPI)
176187
const pdfCanvasEl = document.createElement('canvas');
177188
pdfCanvasEl.className = 'pdf-canvas';
178-
pdfCanvasEl.width = viewport.width;
179-
pdfCanvasEl.height = viewport.height;
189+
setupCanvasHiDPI(pdfCanvasEl, viewport.width, viewport.height);
180190
pdfCanvasEl.dataset.page = pageNum;
181191
pdfCanvasEl.style.display = 'block';
182192
pdfCanvasEl.style.background = 'white';
183193

184-
// Create annotation canvas
194+
// Create annotation canvas (hi-DPI)
185195
const annotationCanvasEl = document.createElement('canvas');
186196
annotationCanvasEl.className = 'annotation-canvas';
187-
annotationCanvasEl.width = viewport.width;
188-
annotationCanvasEl.height = viewport.height;
197+
setupCanvasHiDPI(annotationCanvasEl, viewport.width, viewport.height);
189198
annotationCanvasEl.dataset.page = pageNum;
190199
annotationCanvasEl.style.position = 'absolute';
191200
annotationCanvasEl.style.top = '0';
@@ -202,9 +211,11 @@ async function renderContinuousPage(pageNum) {
202211

203212
// Render PDF page
204213
const pdfCtxEl = pdfCanvasEl.getContext('2d');
214+
const contDpr = getCanvasDPR();
205215
const contRenderContext = {
206216
canvasContext: pdfCtxEl,
207217
viewport: viewport,
218+
transform: contDpr !== 1 ? [contDpr, 0, 0, contDpr, 0, 0] : null,
208219
annotationMode: 0
209220
};
210221
if (state.preferences.thinLines) {

0 commit comments

Comments
 (0)