Skip to content

Commit 6afb5f4

Browse files
committed
feat: Make two syncing PDF viewers
1 parent 8ee5d3b commit 6afb5f4

File tree

1 file changed

+298
-19
lines changed

1 file changed

+298
-19
lines changed

test/report/index.html

Lines changed: 298 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,41 @@
33
<head>
44
<title>jsPDF Test Report</title>
55
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
6+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
67
<style>
78
/* Preserve existing styles that are still needed */
8-
.pdf-container iframe {
9+
.pdf-container {
910
width: 100%;
1011
height: 800px;
11-
border: none;
12+
position: relative;
13+
overflow: hidden;
14+
background: #525659;
15+
display: flex;
16+
flex-direction: column;
17+
}
18+
.pdf-canvas-container {
19+
flex: 1;
20+
position: relative;
21+
overflow: hidden;
22+
cursor: grab;
23+
}
24+
.pdf-canvas-container.panning {
25+
cursor: grabbing;
26+
}
27+
.pdf-canvas-wrapper {
28+
position: absolute;
29+
top: 0;
30+
left: 0;
31+
width: 100%;
32+
height: 100%;
33+
overflow: auto;
34+
}
35+
.pdf-canvas {
36+
position: relative;
37+
margin: 0 auto;
38+
display: block;
39+
user-select: none;
40+
-webkit-user-select: none;
1241
}
1342
.differences {
1443
white-space: pre-wrap;
@@ -63,6 +92,36 @@
6392
width: 1rem;
6493
height: 1rem;
6594
}
95+
.pdf-controls {
96+
padding: 0.75rem;
97+
background: rgba(0, 0, 0, 0.5);
98+
display: flex;
99+
gap: 1rem;
100+
align-items: center;
101+
justify-content: center;
102+
color: white;
103+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
104+
}
105+
.pdf-controls button {
106+
padding: 0.25rem 0.5rem;
107+
border-radius: 0.25rem;
108+
background: rgba(255, 255, 255, 0.2);
109+
border: 1px solid rgba(255, 255, 255, 0.3);
110+
color: white;
111+
cursor: pointer;
112+
}
113+
.pdf-controls button:hover {
114+
background: rgba(255, 255, 255, 0.3);
115+
}
116+
.pdf-controls input {
117+
width: 4rem;
118+
padding: 0.25rem;
119+
border-radius: 0.25rem;
120+
background: rgba(255, 255, 255, 0.1);
121+
border: 1px solid rgba(255, 255, 255, 0.2);
122+
color: white;
123+
text-align: center;
124+
}
66125
</style>
67126
</head>
68127
<body class="antialiased">
@@ -95,9 +154,17 @@ <h3 class="text-sm font-medium text-purple-900">Failing PDFs</h3>
95154

96155
<script>
97156
let selectedItem = null;
157+
let currentPdfState = {
158+
scale: 1.0,
159+
page: 1,
160+
actualPdf: null,
161+
referencePdf: null
162+
};
98163

99164
const documentIcon = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><line x1="10" y1="9" x2="8" y2="9"></line></svg>`;
100165

166+
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
167+
101168
function formatSummary(results) {
102169
return `
103170
<div class="flex items-center justify-between">
@@ -119,7 +186,188 @@ <h3 class="text-sm font-medium text-purple-900">Failing PDFs</h3>
119186
`;
120187
}
121188

122-
function showPdfComparison(item) {
189+
async function loadPdf(url, containerId) {
190+
try {
191+
const loadingTask = pdfjsLib.getDocument(url);
192+
const pdf = await loadingTask.promise;
193+
const container = document.getElementById(containerId);
194+
const canvas = container.querySelector('canvas');
195+
const context = canvas.getContext('2d');
196+
197+
// Store PDF reference
198+
if (containerId === 'actualPdfContainer') {
199+
currentPdfState.actualPdf = pdf;
200+
} else {
201+
currentPdfState.referencePdf = pdf;
202+
}
203+
204+
// Update page count in controls
205+
const pageCount = pdf.numPages;
206+
container.querySelector('.page-count').textContent = pageCount;
207+
208+
// Calculate initial scale to fit width
209+
const page = await pdf.getPage(1);
210+
const viewport = page.getViewport({ scale: 1.0 });
211+
const containerWidth = container.querySelector('.pdf-canvas-container').clientWidth;
212+
const initialScale = (containerWidth - 40) / viewport.width; // 40px padding
213+
currentPdfState.scale = initialScale;
214+
215+
// Update zoom display
216+
document.querySelectorAll('.pdf-controls span:last-of-type').forEach(span => {
217+
span.textContent = `${Math.round(initialScale * 100)}%`;
218+
});
219+
220+
// Render current page
221+
await renderPage(pdf, currentPdfState.page, currentPdfState.scale, canvas, context);
222+
223+
return pdf;
224+
} catch (error) {
225+
console.error('Error loading PDF:', error);
226+
}
227+
}
228+
229+
async function renderPage(pdf, pageNumber, scale, canvas, context) {
230+
const page = await pdf.getPage(pageNumber);
231+
const pixelRatio = window.devicePixelRatio || 1;
232+
const viewport = page.getViewport({ scale: scale * pixelRatio });
233+
234+
canvas.width = viewport.width;
235+
canvas.height = viewport.height;
236+
canvas.style.width = `${viewport.width / pixelRatio}px`;
237+
canvas.style.height = `${viewport.height / pixelRatio}px`;
238+
239+
await page.render({
240+
canvasContext: context,
241+
viewport
242+
}).promise;
243+
}
244+
245+
async function updateBothPdfs() {
246+
if (currentPdfState.actualPdf) {
247+
const actualCanvas = document.querySelector('#actualPdfContainer canvas');
248+
await renderPage(
249+
currentPdfState.actualPdf,
250+
currentPdfState.page,
251+
currentPdfState.scale,
252+
actualCanvas,
253+
actualCanvas.getContext('2d')
254+
);
255+
}
256+
if (currentPdfState.referencePdf) {
257+
const referenceCanvas = document.querySelector('#referencePdfContainer canvas');
258+
await renderPage(
259+
currentPdfState.referencePdf,
260+
currentPdfState.page,
261+
currentPdfState.scale,
262+
referenceCanvas,
263+
referenceCanvas.getContext('2d')
264+
);
265+
}
266+
}
267+
268+
function createPdfViewer(id, title) {
269+
return `
270+
<div class="card">
271+
<div class="p-3 border-b">
272+
<h3 class="font-medium text-purple-900">${title}</h3>
273+
</div>
274+
<div id="${id}" class="pdf-container">
275+
<div class="pdf-controls">
276+
<button onclick="changePage(-1)">◀</button>
277+
<span>Page <input type="number" value="1" min="1" onchange="setPage(this.value)"> of <span class="page-count">0</span></span>
278+
<button onclick="changePage(1)">▶</button>
279+
<button onclick="changeZoom(-0.1)">-</button>
280+
<span>${Math.round(currentPdfState.scale * 100)}%</span>
281+
<button onclick="changeZoom(0.1)">+</button>
282+
<button onclick="fitToWidth()">Fit</button>
283+
</div>
284+
<div class="pdf-canvas-container" onmousedown="startPan(event, this)">
285+
<div class="pdf-canvas-wrapper" onwheel="handleScroll(event, this)">
286+
<canvas class="pdf-canvas"></canvas>
287+
</div>
288+
</div>
289+
</div>
290+
</div>
291+
`;
292+
}
293+
294+
// Add scroll synchronization
295+
function handleScroll(e, wrapper) {
296+
const containers = document.querySelectorAll('.pdf-canvas-wrapper');
297+
containers.forEach(container => {
298+
if (container !== wrapper) {
299+
container.scrollTop = wrapper.scrollTop;
300+
container.scrollLeft = wrapper.scrollLeft;
301+
}
302+
});
303+
}
304+
305+
// Add panning functionality
306+
let isPanning = false;
307+
let startPoint = { x: 0, y: 0 };
308+
let scrollPositions = { x: 0, y: 0 };
309+
let activePanContainer = null;
310+
311+
function startPan(e, container) {
312+
if (e.button !== 0) return; // Only left mouse button
313+
isPanning = true;
314+
activePanContainer = container;
315+
container.classList.add('panning');
316+
317+
startPoint = { x: e.clientX, y: e.clientY };
318+
const wrapper = container.querySelector('.pdf-canvas-wrapper');
319+
scrollPositions = {
320+
x: wrapper.scrollLeft,
321+
y: wrapper.scrollTop
322+
};
323+
324+
document.addEventListener('mousemove', handlePan);
325+
document.addEventListener('mouseup', endPan);
326+
e.preventDefault();
327+
}
328+
329+
function handlePan(e) {
330+
if (!isPanning || !activePanContainer) return;
331+
332+
const dx = startPoint.x - e.clientX;
333+
const dy = startPoint.y - e.clientY;
334+
335+
const containers = document.querySelectorAll('.pdf-canvas-wrapper');
336+
containers.forEach(wrapper => {
337+
wrapper.scrollLeft = scrollPositions.x + dx;
338+
wrapper.scrollTop = scrollPositions.y + dy;
339+
});
340+
341+
e.preventDefault();
342+
}
343+
344+
function endPan() {
345+
if (!isPanning || !activePanContainer) return;
346+
347+
isPanning = false;
348+
activePanContainer.classList.remove('panning');
349+
activePanContainer = null;
350+
351+
document.removeEventListener('mousemove', handlePan);
352+
document.removeEventListener('mouseup', endPan);
353+
}
354+
355+
async function fitToWidth() {
356+
if (!currentPdfState.actualPdf) return;
357+
358+
const page = await currentPdfState.actualPdf.getPage(currentPdfState.page);
359+
const viewport = page.getViewport({ scale: 1.0 });
360+
const container = document.querySelector('.pdf-canvas-container');
361+
const newScale = (container.clientWidth - 40) / viewport.width;
362+
363+
currentPdfState.scale = newScale;
364+
document.querySelectorAll('.pdf-controls span:last-of-type').forEach(span => {
365+
span.textContent = `${Math.round(newScale * 100)}%`;
366+
});
367+
await updateBothPdfs();
368+
}
369+
370+
async function showPdfComparison(item) {
123371
const details = document.getElementById('details');
124372

125373
// Update selected state in sidebar
@@ -129,29 +377,23 @@ <h3 class="text-sm font-medium text-purple-900">Failing PDFs</h3>
129377
document.getElementById(item.id).classList.add('selected');
130378
selectedItem = item.id;
131379

380+
// Reset PDF state
381+
currentPdfState = {
382+
scale: 1.0,
383+
page: 1,
384+
actualPdf: null,
385+
referencePdf: null
386+
};
387+
132388
details.innerHTML = `
133389
<div class="card">
134390
<div class="p-4 border-b">
135391
<h2 class="text-lg font-semibold text-purple-900">${item.name}</h2>
136392
</div>
137393
<div class="p-4">
138394
<div class="grid grid-cols-2 gap-6">
139-
<div class="card">
140-
<div class="p-3 border-b">
141-
<h3 class="font-medium text-purple-900">Actual PDF</h3>
142-
</div>
143-
<div class="pdf-container">
144-
<iframe src="/${item.actualPdf}"></iframe>
145-
</div>
146-
</div>
147-
<div class="card">
148-
<div class="p-3 border-b">
149-
<h3 class="font-medium text-purple-900">Reference PDF</h3>
150-
</div>
151-
<div class="pdf-container">
152-
<iframe src="/${item.referencePdf}"></iframe>
153-
</div>
154-
</div>
395+
${createPdfViewer('actualPdfContainer', 'Actual PDF')}
396+
${createPdfViewer('referencePdfContainer', 'Reference PDF')}
155397
</div>
156398
${item.error ? `
157399
<div class="mt-6">
@@ -168,6 +410,43 @@ <h3 class="font-medium text-purple-900">Differences</h3>
168410
</div>
169411
</div>
170412
`;
413+
414+
// Load both PDFs
415+
await Promise.all([
416+
loadPdf(item.actualPdf, 'actualPdfContainer'),
417+
loadPdf(item.referencePdf, 'referencePdfContainer')
418+
]);
419+
}
420+
421+
async function changePage(delta) {
422+
const newPage = currentPdfState.page + delta;
423+
if (currentPdfState.actualPdf && newPage >= 1 && newPage <= currentPdfState.actualPdf.numPages) {
424+
currentPdfState.page = newPage;
425+
document.querySelectorAll('.pdf-controls input[type="number"]').forEach(input => {
426+
input.value = newPage;
427+
});
428+
await updateBothPdfs();
429+
}
430+
}
431+
432+
async function setPage(pageNum) {
433+
const newPage = parseInt(pageNum, 10);
434+
if (currentPdfState.actualPdf && newPage >= 1 && newPage <= currentPdfState.actualPdf.numPages) {
435+
currentPdfState.page = newPage;
436+
document.querySelectorAll('.pdf-controls input[type="number"]').forEach(input => {
437+
input.value = newPage;
438+
});
439+
await updateBothPdfs();
440+
}
441+
}
442+
443+
async function changeZoom(delta) {
444+
const newScale = Math.max(0.1, Math.min(5.0, currentPdfState.scale + delta));
445+
currentPdfState.scale = newScale;
446+
document.querySelectorAll('.pdf-controls span:last-of-type').forEach(span => {
447+
span.textContent = `${Math.round(newScale * 100)}%`;
448+
});
449+
await updateBothPdfs();
171450
}
172451

173452
async function init() {

0 commit comments

Comments
 (0)