diff --git a/src/js/logic/bookmark-pdf.ts b/src/js/logic/bookmark-pdf.ts
index a66b6ac5..a1e775a0 100644
--- a/src/js/logic/bookmark-pdf.ts
+++ b/src/js/logic/bookmark-pdf.ts
@@ -17,6 +17,10 @@ const modalContainer = document.getElementById('modal-container');
let isPickingDestination = false;
let currentPickingCallback = null;
let destinationMarker = null;
+let savedModalOverlay = null;
+let savedModal = null;
+let currentViewport = null;
+let currentZoom = 1.0;
function showInputModal(title, fields = [], defaultValues = {}) {
return new Promise((resolve) => {
@@ -75,7 +79,7 @@ function showInputModal(title, fields = [], defaultValues = {}) {
+ class="w-full px-2 py-1 border border-gray-300 rounded text-sm" step="1" />
@@ -238,27 +242,97 @@ function showInputModal(title, fields = [], defaultValues = {}) {
// Visual destination picker
if (pickDestBtn) {
pickDestBtn.addEventListener('click', () => {
- startDestinationPicking((page, x, y) => {
+ // Store modal references
+ savedModalOverlay = overlay;
+ savedModal = modal;
+
+ // Hide modal completely
+ overlay.style.display = 'none';
+
+ startDestinationPicking((page, pdfX, pdfY) => {
const destPageInput = modal.querySelector('#modal-dest-page');
const destXInput = modal.querySelector('#modal-dest-x');
const destYInput = modal.querySelector('#modal-dest-y');
if (destPageInput) destPageInput.value = page;
- if (destXInput) destXInput.value = Math.round(x);
- if (destYInput) destYInput.value = Math.round(y);
-
- // Minimize modal to corner
- const modalOverlay = document.getElementById('active-modal-overlay');
- const activeModal = document.getElementById('active-modal');
- if (modalOverlay && activeModal) {
- modalOverlay.style.background = 'transparent';
- modalOverlay.style.pointerEvents = 'none';
- activeModal.classList.add('modal-minimized');
- activeModal.style.pointerEvents = 'auto';
- }
+ if (destXInput) destXInput.value = Math.round(pdfX);
+ if (destYInput) destYInput.value = Math.round(pdfY);
+
+ // Restore modal
+ overlay.style.display = '';
+
+ // Update preview to show the destination after a short delay to ensure modal is visible
+ setTimeout(() => {
+ updateDestinationPreview();
+ }, 100);
});
});
}
+
+ // Add validation for page input
+ const destPageInput = modal.querySelector('#modal-dest-page');
+ if (destPageInput) {
+ destPageInput.addEventListener('input', (e) => {
+ const value = parseInt(e.target.value);
+ const maxPages = parseInt(e.target.max) || 1;
+ if (isNaN(value) || value < 1) {
+ e.target.value = 1;
+ } else if (value > maxPages) {
+ e.target.value = maxPages;
+ } else {
+ e.target.value = Math.floor(value);
+ }
+ updateDestinationPreview();
+ });
+
+ destPageInput.addEventListener('blur', (e) => {
+ const value = parseInt(e.target.value);
+ const maxPages = parseInt(e.target.max) || 1;
+ if (isNaN(value) || value < 1) {
+ e.target.value = 1;
+ } else if (value > maxPages) {
+ e.target.value = maxPages;
+ } else {
+ e.target.value = Math.floor(value);
+ }
+ updateDestinationPreview();
+ });
+ }
+
+ // Function to update destination preview
+ function updateDestinationPreview() {
+ if (!pdfJsDoc) return;
+
+ const destPageInput = modal.querySelector('#modal-dest-page');
+ const destXInput = modal.querySelector('#modal-dest-x');
+ const destYInput = modal.querySelector('#modal-dest-y');
+ const destZoomSelect = modal.querySelector('#modal-dest-zoom');
+
+ const pageNum = destPageInput ? parseInt(destPageInput.value) : currentPage;
+ const x = destXInput ? parseFloat(destXInput.value) : null;
+ const y = destYInput ? parseFloat(destYInput.value) : null;
+ const zoom = destZoomSelect ? destZoomSelect.value : null;
+
+ if (pageNum >= 1 && pageNum <= pdfJsDoc.numPages) {
+ // Render the page with zoom if specified
+ renderPageWithDestination(pageNum, x, y, zoom);
+ }
+ }
+
+ // Add listeners for X, Y, and zoom changes
+ const destXInput = modal.querySelector('#modal-dest-x');
+ const destYInput = modal.querySelector('#modal-dest-y');
+ const destZoomSelect = modal.querySelector('#modal-dest-zoom');
+
+ if (destXInput) {
+ destXInput.addEventListener('input', updateDestinationPreview);
+ }
+ if (destYInput) {
+ destYInput.addEventListener('input', updateDestinationPreview);
+ }
+ if (destZoomSelect) {
+ destZoomSelect.addEventListener('change', updateDestinationPreview);
+ }
updatePreview();
@@ -359,14 +433,18 @@ function cancelDestinationPicking() {
destinationMarker.remove();
destinationMarker = null;
}
+
+ // Remove coordinate display
+ const coordDisplay = document.getElementById('destination-coord-display');
+ if (coordDisplay) {
+ coordDisplay.remove();
+ }
- // Restore modal if minimized
- const modalOverlay = document.getElementById('active-modal-overlay');
- const activeModal = document.getElementById('active-modal');
- if (modalOverlay && activeModal) {
- modalOverlay.style.background = 'rgba(0, 0, 0, 0.5)';
- modalOverlay.style.pointerEvents = 'auto';
- activeModal.classList.remove('modal-minimized');
+ // Restore modal if it was hidden
+ if (savedModalOverlay) {
+ savedModalOverlay.style.display = '';
+ savedModalOverlay = null;
+ savedModal = null;
}
}
@@ -405,17 +483,36 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
- canvas.addEventListener('click', (e) => {
+ canvas.addEventListener('click', async (e) => {
if (!isPickingDestination || !currentPickingCallback) return;
const rect = canvas.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
+ const canvasX = e.clientX - rect.left;
+ const canvasY = e.clientY - rect.top;
- // Remove old marker
+ // Get viewport for coordinate conversion
+ let viewport = currentViewport;
+ if (!viewport) {
+ const page = await pdfJsDoc.getPage(currentPage);
+ viewport = page.getViewport({ scale: currentZoom });
+ }
+
+ // Convert canvas pixel coordinates to PDF coordinates
+ // The canvas CSS size matches viewport dimensions, so coordinates map directly
+ // PDF uses bottom-left origin, canvas uses top-left
+ const scaleX = viewport.width / rect.width;
+ const scaleY = viewport.height / rect.height;
+ const pdfX = canvasX * scaleX;
+ const pdfY = viewport.height - (canvasY * scaleY);
+
+ // Remove old marker and coordinate display
if (destinationMarker) {
destinationMarker.remove();
}
+ const oldCoordDisplay = document.getElementById('destination-coord-display');
+ if (oldCoordDisplay) {
+ oldCoordDisplay.remove();
+ }
// Create visual marker
destinationMarker = document.createElement('div');
@@ -427,13 +524,24 @@ document.addEventListener('DOMContentLoaded', () => {
`;
- destinationMarker.style.left =
- x + rect.left - canvasWrapper.offsetLeft + 'px';
- destinationMarker.style.top = y + rect.top - canvasWrapper.offsetTop + 'px';
+ const canvasRect = canvas.getBoundingClientRect();
+ const wrapperRect = canvasWrapper.getBoundingClientRect();
+ destinationMarker.style.position = 'absolute';
+ destinationMarker.style.left = (canvasX + canvasRect.left - wrapperRect.left) + 'px';
+ destinationMarker.style.top = (canvasY + canvasRect.top - wrapperRect.top) + 'px';
canvasWrapper.appendChild(destinationMarker);
+
+ // Create persistent coordinate display
+ const coordDisplay = document.createElement('div');
+ coordDisplay.id = 'destination-coord-display';
+ coordDisplay.className = 'absolute bg-blue-500 text-white px-2 py-1 rounded text-xs font-mono z-50 pointer-events-none';
+ coordDisplay.style.left = (canvasX + canvasRect.left - wrapperRect.left + 20) + 'px';
+ coordDisplay.style.top = (canvasY + canvasRect.top - wrapperRect.top - 30) + 'px';
+ coordDisplay.textContent = `X: ${Math.round(pdfX)}, Y: ${Math.round(pdfY)}`;
+ canvasWrapper.appendChild(coordDisplay);
- // Call callback with coordinates
- currentPickingCallback(currentPage, x, y);
+ // Call callback with PDF coordinates
+ currentPickingCallback(currentPage, pdfX, pdfY);
// End picking mode
setTimeout(() => {
@@ -537,6 +645,10 @@ const prevPageBtn = document.getElementById('prev-page');
const nextPageBtn = document.getElementById('next-page');
const gotoPageInput = document.getElementById('goto-page');
const gotoBtn = document.getElementById('goto-btn');
+const zoomInBtn = document.getElementById('zoom-in-btn');
+const zoomOutBtn = document.getElementById('zoom-out-btn');
+const zoomFitBtn = document.getElementById('zoom-fit-btn');
+const zoomIndicator = document.getElementById('zoom-indicator');
const addTopLevelBtn = document.getElementById('add-top-level-btn');
const titleInput = document.getElementById('bookmark-title');
const treeList = document.getElementById('bookmark-tree-list');
@@ -940,16 +1052,80 @@ jsonInput.addEventListener('change', async (e) => {
}
});
-async function renderPage(num) {
+async function renderPage(num, zoom = null, destX = null, destY = null) {
if (!pdfJsDoc) return;
const page = await pdfJsDoc.getPage(num);
- const viewport = page.getViewport({ scale: 1.5 });
+
+ let zoomScale = currentZoom;
+ if (zoom !== null && zoom !== '' && zoom !== '0') {
+ zoomScale = parseFloat(zoom) / 100;
+ }
+
+ const dpr = window.devicePixelRatio || 1;
+
+ let viewport = page.getViewport({ scale: zoomScale });
+ currentViewport = viewport;
- canvas.height = viewport.height;
- canvas.width = viewport.width;
+ canvas.height = viewport.height * dpr;
+ canvas.width = viewport.width * dpr;
+
+ // Set CSS size to maintain aspect ratio (this is what the browser displays)
+ canvas.style.width = viewport.width + 'px';
+ canvas.style.height = viewport.height + 'px';
+
+ // Scale the canvas context to match device pixel ratio
+ ctx.scale(dpr, dpr);
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
+
+ // Draw destination marker if coordinates are provided
+ if (destX !== null && destY !== null) {
+ const canvasX = destX;
+ const canvasY = viewport.height - destY; // Flip Y axis (PDF bottom-left, canvas top-left)
+
+ // Draw marker on canvas with animation effect
+ ctx.save();
+ ctx.strokeStyle = '#3b82f6';
+ ctx.fillStyle = '#3b82f6';
+ ctx.lineWidth = 3;
+
+ ctx.shadowBlur = 10;
+ ctx.shadowColor = 'rgba(59, 130, 246, 0.5)';
+ ctx.beginPath();
+ ctx.arc(canvasX, canvasY, 12, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+
+ // Draw crosshair
+ ctx.beginPath();
+ ctx.moveTo(canvasX - 15, canvasY);
+ ctx.lineTo(canvasX + 15, canvasY);
+ ctx.moveTo(canvasX, canvasY - 15);
+ ctx.lineTo(canvasX, canvasY + 15);
+ ctx.stroke();
+
+ // Draw inner circle
+ ctx.beginPath();
+ ctx.arc(canvasX, canvasY, 6, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.stroke();
+
+ // Draw coordinate text background
+ const text = `X: ${Math.round(destX)}, Y: ${Math.round(destY)}`;
+ ctx.font = 'bold 12px monospace';
+ const textMetrics = ctx.measureText(text);
+ const textWidth = textMetrics.width;
+ const textHeight = 18;
+
+ ctx.fillStyle = 'rgba(59, 130, 246, 0.95)';
+ ctx.fillRect(canvasX + 18, canvasY - 25, textWidth + 10, textHeight);
+
+ ctx.fillStyle = 'white';
+ ctx.fillText(text, canvasX + 23, canvasY - 10);
+
+ ctx.restore();
+ }
pageIndicator.textContent = `Page ${num} / ${pdfJsDoc.numPages}`;
gotoPageInput.value = num;
@@ -957,6 +1133,10 @@ async function renderPage(num) {
currentPageDisplay.textContent = num;
}
+async function renderPageWithDestination(pageNum, x, y, zoom) {
+ await renderPage(pageNum, zoom, x, y);
+}
+
prevPageBtn.addEventListener('click', () => {
if (currentPage > 1) renderPage(currentPage - 1);
});
@@ -976,6 +1156,35 @@ gotoPageInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') gotoBtn.click();
});
+// Zoom controls
+function updateZoomIndicator() {
+ if (zoomIndicator) {
+ zoomIndicator.textContent = `${Math.round(currentZoom * 100)}%`;
+ }
+}
+
+zoomInBtn.addEventListener('click', () => {
+ currentZoom = Math.min(currentZoom + 0.05, 2.0); // Max 200%, increment by 5%
+ updateZoomIndicator();
+ renderPage(currentPage);
+});
+
+zoomOutBtn.addEventListener('click', () => {
+ currentZoom = Math.max(currentZoom - 0.05, 0.25); // Min 25%, decrement by 5%
+ updateZoomIndicator();
+ renderPage(currentPage);
+});
+
+zoomFitBtn.addEventListener('click', async () => {
+ if (!pdfJsDoc) return;
+ currentZoom = 1.0;
+ updateZoomIndicator();
+ renderPage(currentPage);
+});
+
+// Initialize zoom indicator
+updateZoomIndicator();
+
searchInput.addEventListener('input', (e) => {
searchQuery = e.target.value.toLowerCase();
renderBookmarkTree();
@@ -1210,8 +1419,27 @@ function createNodeElement(node, level = 0) {
Page ${node.page}
`;
- titleDiv.addEventListener('click', () => {
- renderPage(node.page);
+ titleDiv.addEventListener('click', async () => {
+ // Check if bookmark has a custom destination
+ if (node.destX !== null || node.destY !== null || node.zoom !== null) {
+ // Render page with destination highlighted and zoom applied
+ await renderPageWithDestination(node.page, node.destX, node.destY, node.zoom);
+
+ // Highlight the destination briefly (2 seconds)
+ setTimeout(() => {
+ // Re-render without highlight but keep the zoom if it was set
+ if (node.zoom !== null && node.zoom !== '' && node.zoom !== '0') {
+ // Keep the bookmark's zoom for a moment, then restore current zoom
+ setTimeout(() => {
+ renderPage(node.page);
+ }, 1000);
+ } else {
+ renderPage(node.page);
+ }
+ }, 2000);
+ } else {
+ renderPage(node.page);
+ }
if (window.innerWidth < 1024) {
showViewerBtn.click();
}
diff --git a/src/pages/bookmark.html b/src/pages/bookmark.html
index 09237eb3..652d53bb 100644
--- a/src/pages/bookmark.html
+++ b/src/pages/bookmark.html
@@ -6,7 +6,6 @@
Advanced PDF Bookmark Tool
-