pdf-diff-viewer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js ADDED
@@ -0,0 +1,588 @@
1
+ angular.module("pdfDiffApp", [])
2
+ .controller("MainCtrl", function ($scope) {
3
+
4
+ pdfjsLib.GlobalWorkerOptions.workerSrc =
5
+ "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js";
6
+
7
+ const SCALE = 3.0; // ~300 DPI
8
+ const MAX_SHIFT = 3; // pixel search radius for alignment
9
+ const DILATION_RADIUS = 0; // set >0 to expand highlights around small diffs
10
+ const COLOR_TOLERANCE = 120; // sum of channel deltas required to call a pixel different
11
+ const MIN_HIGHLIGHT_AREA = 60; // pixels - minimum area to highlight
12
+ const MIN_WORD_SIZE = 8; // pixels - minimum word box width/height to highlight
13
+ const HIGHLIGHT_ALPHA = 0.32;
14
+ async function renderPageToCanvas(pdf, pageNum, canvas) {
15
+ const page = await pdf.getPage(pageNum);
16
+ const viewport = page.getViewport({ scale: SCALE });
17
+
18
+ // ❗ Ensure exact same dimensions every time
19
+ canvas.width = Math.floor(viewport.width);
20
+ canvas.height = Math.floor(viewport.height);
21
+
22
+ const ctx = canvas.getContext("2d");
23
+
24
+ // ❗ Clear previous page completely (prevents overlap artifacts)
25
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
26
+ ctx.imageSmoothingEnabled = false;
27
+
28
+ await page.render({
29
+ canvasContext: ctx,
30
+ viewport
31
+ }).promise;
32
+
33
+ const words = await extractWordBoxes(page, viewport);
34
+ return { words };
35
+ }
36
+
37
+ async function extractWordBoxes(page, viewport) {
38
+ const textContent = await page.getTextContent({ normalizeWhitespace: true });
39
+ const boxes = [];
40
+
41
+ textContent.items.forEach((item) => {
42
+ const text = (item.str || "").trim();
43
+ if (!text) {
44
+ return;
45
+ }
46
+
47
+ const transform = pdfjsLib.Util.transform(viewport.transform, item.transform);
48
+ const x = transform[4];
49
+ const y = transform[5];
50
+
51
+ const width = (item.width || 0) * viewport.scale;
52
+ const glyphHeight = Math.hypot(transform[2], transform[3]);
53
+ const height = glyphHeight || ((item.height || 0) * viewport.scale);
54
+
55
+ if (!width || !height) {
56
+ return;
57
+ }
58
+
59
+ const charWidth = width / text.length;
60
+ if (!isFinite(charWidth) || charWidth <= 0) {
61
+ return;
62
+ }
63
+
64
+ const baseY = y - height;
65
+ let cursorX = x;
66
+
67
+ text.split(/(\s+)/).forEach((segment) => {
68
+ if (!segment) {
69
+ return;
70
+ }
71
+
72
+ const segmentWidth = charWidth * segment.length;
73
+ if (!segment.trim()) {
74
+ cursorX += segmentWidth;
75
+ return;
76
+ }
77
+
78
+ const paddingX = charWidth * 0.18;
79
+ const paddingY = height * 0.15;
80
+ const box = padBox({
81
+ x: cursorX,
82
+ y: baseY,
83
+ width: segmentWidth,
84
+ height
85
+ }, paddingX, paddingY);
86
+
87
+ boxes.push(box);
88
+ cursorX += segmentWidth;
89
+ });
90
+ });
91
+
92
+ return boxes;
93
+ }
94
+
95
+ // Pad a rendered canvas so both sides share identical dimensions (avoids overlap/misalignment).
96
+ function padCanvas(srcCanvas, targetWidth, targetHeight) {
97
+ if (srcCanvas.width === targetWidth && srcCanvas.height === targetHeight) {
98
+ return srcCanvas;
99
+ }
100
+
101
+ const padded = document.createElement("canvas");
102
+ padded.width = targetWidth;
103
+ padded.height = targetHeight;
104
+
105
+ const ctx = padded.getContext("2d");
106
+ ctx.fillStyle = "white";
107
+ ctx.fillRect(0, 0, targetWidth, targetHeight);
108
+ ctx.drawImage(srcCanvas, 0, 0);
109
+ return padded;
110
+ }
111
+
112
+ // Shift a canvas by (dx, dy) on a white background and return ImageData.
113
+ function getShiftedImageData(srcCanvas, width, height, dx, dy) {
114
+ const temp = document.createElement("canvas");
115
+ temp.width = width;
116
+ temp.height = height;
117
+ const ctx = temp.getContext("2d");
118
+ ctx.fillStyle = "white";
119
+ ctx.fillRect(0, 0, width, height);
120
+ ctx.drawImage(srcCanvas, dx, dy);
121
+ return ctx.getImageData(0, 0, width, height);
122
+ }
123
+
124
+ function pixelDelta(dataA, dataB, index) {
125
+ return Math.abs(dataA[index] - dataB[index]) +
126
+ Math.abs(dataA[index + 1] - dataB[index + 1]) +
127
+ Math.abs(dataA[index + 2] - dataB[index + 2]);
128
+ }
129
+
130
+ // Count different pixels between two images given a tolerance.
131
+ function countDiffPixels(imgA, imgB, tolerance) {
132
+ const dataA = imgA.data;
133
+ const dataB = imgB.data;
134
+ let diff = 0;
135
+ for (let i = 0; i < dataA.length; i += 4) {
136
+ if (pixelDelta(dataA, dataB, i) > tolerance) {
137
+ diff++;
138
+ }
139
+ }
140
+ return diff;
141
+ }
142
+
143
+ // Search small translations to find the lowest diff count (helps with minor reflows).
144
+ function findBestOffset(imgA, paddedB, width, height, tolerance) {
145
+ let best = { diff: Infinity, dx: 0, dy: 0 };
146
+ for (let dy = -MAX_SHIFT; dy <= MAX_SHIFT; dy++) {
147
+ for (let dx = -MAX_SHIFT; dx <= MAX_SHIFT; dx++) {
148
+ const shiftedB = getShiftedImageData(paddedB, width, height, dx, dy);
149
+ const diffCount = countDiffPixels(imgA, shiftedB, tolerance);
150
+ if (diffCount < best.diff) {
151
+ best = { diff: diffCount, dx, dy };
152
+ }
153
+ }
154
+ }
155
+ return best;
156
+ }
157
+
158
+ // Build a diff image between two ImageData objects using the color tolerance.
159
+ function buildDiffImage(imgA, imgB, diffImage, tolerance) {
160
+ const target = diffImage.data;
161
+ const dataA = imgA.data;
162
+ const dataB = imgB.data;
163
+ let diffPixels = 0;
164
+
165
+ for (let i = 0; i < dataA.length; i += 4) {
166
+ if (pixelDelta(dataA, dataB, i) > tolerance) {
167
+ target[i] = 255;
168
+ target[i + 1] = 0;
169
+ target[i + 2] = 0;
170
+ target[i + 3] = 255;
171
+ diffPixels++;
172
+ } else {
173
+ target[i] = 0;
174
+ target[i + 1] = 0;
175
+ target[i + 2] = 0;
176
+ target[i + 3] = 0;
177
+ }
178
+ }
179
+
180
+ return diffPixels;
181
+ }
182
+
183
+ // Enlarge diff areas so tiny letter changes highlight the nearby word/line.
184
+ function dilateDiffMask(diffImage, width, height, radius = 0) {
185
+ const src = diffImage.data;
186
+ const mask = new Uint8Array(width * height);
187
+
188
+ // Build binary mask from existing diff pixels.
189
+ for (let i = 0; i < width * height; i++) {
190
+ if (src[i * 4] > 0) {
191
+ mask[i] = 1;
192
+ }
193
+ }
194
+
195
+ // Start with the base mask; optionally expand.
196
+ const expanded = new Uint8Array(mask);
197
+ if (radius > 0) {
198
+ for (let y = 0; y < height; y++) {
199
+ for (let x = 0; x < width; x++) {
200
+ const idx = y * width + x;
201
+ if (!mask[idx]) continue;
202
+ const yMin = Math.max(0, y - radius);
203
+ const yMax = Math.min(height - 1, y + radius);
204
+ const xMin = Math.max(0, x - radius);
205
+ const xMax = Math.min(width - 1, x + radius);
206
+ for (let ny = yMin; ny <= yMax; ny++) {
207
+ for (let nx = xMin; nx <= xMax; nx++) {
208
+ expanded[ny * width + nx] = 1;
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+
215
+ // Rewrite the diffImage with the expanded mask in solid red.
216
+ for (let i = 0; i < width * height; i++) {
217
+ const base = i * 4;
218
+ if (expanded[i]) {
219
+ src[base] = 255;
220
+ src[base + 1] = 0;
221
+ src[base + 2] = 0;
222
+ src[base + 3] = 255;
223
+ }
224
+ }
225
+ }
226
+
227
+ function extractDiffBoxes(diffImage, width, height, minArea = 25) {
228
+ const data = diffImage.data;
229
+ const visited = new Uint8Array(width * height);
230
+ const boxes = [];
231
+
232
+ const directions = [1, -1, width, -width];
233
+
234
+ for (let idx = 0; idx < width * height; idx++) {
235
+ if (visited[idx]) continue;
236
+ if (data[idx * 4 + 3] === 0) continue;
237
+
238
+ let minX = idx % width;
239
+ let maxX = minX;
240
+ let minY = Math.floor(idx / width);
241
+ let maxY = minY;
242
+
243
+ const stack = [idx];
244
+ visited[idx] = 1;
245
+
246
+ while (stack.length) {
247
+ const current = stack.pop();
248
+ const cx = current % width;
249
+ const cy = Math.floor(current / width);
250
+
251
+ if (cx < minX) minX = cx;
252
+ if (cx > maxX) maxX = cx;
253
+ if (cy < minY) minY = cy;
254
+ if (cy > maxY) maxY = cy;
255
+
256
+ for (const dir of directions) {
257
+ const next = current + dir;
258
+ if (next < 0 || next >= width * height) continue;
259
+ const nx = next % width;
260
+ const ny = Math.floor(next / width);
261
+ if (Math.abs(nx - cx) + Math.abs(ny - cy) !== 1) continue;
262
+ if (visited[next]) continue;
263
+ if (data[next * 4 + 3] === 0) continue;
264
+ visited[next] = 1;
265
+ stack.push(next);
266
+ }
267
+ }
268
+
269
+ const area = (maxX - minX + 1) * (maxY - minY + 1);
270
+ if (area >= minArea) {
271
+ boxes.push({
272
+ x: minX,
273
+ y: minY,
274
+ width: maxX - minX + 1,
275
+ height: maxY - minY + 1
276
+ });
277
+ }
278
+ }
279
+
280
+ return boxes;
281
+ }
282
+
283
+ function drawHighlightBoxes(ctx, boxes, color = 'red') {
284
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
285
+ const alpha = HIGHLIGHT_ALPHA;
286
+ if (color === 'red') {
287
+ ctx.fillStyle = `rgba(255, 0, 0, ${alpha})`;
288
+ } else if (color === 'green') {
289
+ ctx.fillStyle = `rgba(0, 200, 0, ${alpha})`;
290
+ }
291
+ boxes.forEach(({ x, y, width, height }) => {
292
+ ctx.fillRect(x, y, width, height);
293
+ });
294
+ }
295
+
296
+ function rectsIntersect(a, b) {
297
+ return (
298
+ a.x < b.x + b.width &&
299
+ a.x + a.width > b.x &&
300
+ a.y < b.y + b.height &&
301
+ a.y + a.height > b.y
302
+ );
303
+ }
304
+
305
+ function dedupeBoxes(boxes) {
306
+ const seen = new Set();
307
+ const result = [];
308
+ boxes.forEach((box) => {
309
+ const key = [Math.round(box.x), Math.round(box.y), Math.round(box.width), Math.round(box.height)].join(":");
310
+ if (seen.has(key)) {
311
+ return;
312
+ }
313
+ seen.add(key);
314
+ result.push({ x: box.x, y: box.y, width: box.width, height: box.height });
315
+ });
316
+ return result;
317
+ }
318
+
319
+ function padBox(box, paddingX, paddingY) {
320
+ const x = Math.max(0, box.x - paddingX);
321
+ const y = Math.max(0, box.y - paddingY);
322
+ return {
323
+ x,
324
+ y,
325
+ width: Math.max(1, box.width + paddingX * 2),
326
+ height: Math.max(1, box.height + paddingY * 2)
327
+ };
328
+ }
329
+
330
+ function mapDiffsToWordBoxes(diffBoxes, wordBoxes) {
331
+ if (!wordBoxes || !wordBoxes.length) {
332
+ return diffBoxes;
333
+ }
334
+
335
+ const matched = [];
336
+
337
+ diffBoxes.forEach((diffBox) => {
338
+ let found = false;
339
+ for (const word of wordBoxes) {
340
+ if (rectsIntersect(diffBox, word)) {
341
+ // Skip tiny word boxes (likely rendering noise)
342
+ if (word.width >= MIN_WORD_SIZE && word.height >= MIN_WORD_SIZE) {
343
+ matched.push(word);
344
+ found = true;
345
+ }
346
+ }
347
+ }
348
+ if (!found) {
349
+ matched.push(diffBox);
350
+ }
351
+ });
352
+
353
+ return dedupeBoxes(matched);
354
+ }
355
+
356
+ function translateBoxes(boxes, dx, dy) {
357
+ return boxes.map((box) => ({
358
+ x: box.x + dx,
359
+ y: box.y + dy,
360
+ width: box.width,
361
+ height: box.height
362
+ }));
363
+ }
364
+
365
+ function offsetWordBoxes(words, crop) {
366
+ if (!crop) return words;
367
+ return words.map(box => ({
368
+ ...box,
369
+ x: box.x - crop.x,
370
+ y: box.y - crop.y
371
+ }));
372
+ }
373
+
374
+ function canvasToImageData(canvas) {
375
+ const ctx = canvas.getContext("2d");
376
+ return ctx.getImageData(0, 0, canvas.width, canvas.height);
377
+ }
378
+
379
+ function downloadZip(zip, filename) {
380
+ zip.generateAsync({ type: "blob" }).then(function (blob) {
381
+ const a = document.createElement("a");
382
+ a.href = URL.createObjectURL(blob);
383
+ a.download = filename;
384
+ document.body.appendChild(a);
385
+ a.click();
386
+ document.body.removeChild(a);
387
+ });
388
+ }
389
+
390
+ function applyCrop(canvas, region) {
391
+ if (!region) return canvas;
392
+ const cropped = document.createElement("canvas");
393
+ cropped.width = region.width;
394
+ cropped.height = region.height;
395
+ const ctx = cropped.getContext("2d");
396
+ ctx.drawImage(
397
+ canvas,
398
+ region.x, region.y, region.width, region.height,
399
+ 0, 0, region.width, region.height
400
+ );
401
+ return cropped;
402
+ }
403
+
404
+ function applyMasks(diffImage, masks) {
405
+ if (!masks?.length) return;
406
+ const data = diffImage.data;
407
+ masks.forEach(({ x, y, width, height }) => {
408
+ for (let row = y; row < y + height; row++) {
409
+ for (let col = x; col < x + width; col++) {
410
+ const idx = (row * diffImage.width + col) * 4;
411
+ data[idx] = data[idx + 1] = data[idx + 2] = data[idx + 3] = 0;
412
+ }
413
+ }
414
+ });
415
+ }
416
+
417
+ // === Blend diff over a base image ===
418
+ function overlayDiff(baseCanvas, diffCanvas, opacity = 0.3) {
419
+ const overlay = document.createElement("canvas");
420
+ overlay.width = baseCanvas.width;
421
+ overlay.height = baseCanvas.height;
422
+
423
+ const ctx = overlay.getContext("2d");
424
+
425
+ ctx.drawImage(baseCanvas, 0, 0);
426
+ ctx.globalAlpha = opacity;
427
+ ctx.drawImage(diffCanvas, 0, 0);
428
+ ctx.globalAlpha = 1;
429
+ ctx.imageSmoothingEnabled = false;
430
+
431
+ return overlay;
432
+ }
433
+
434
+ $scope.compare = async function () {
435
+ const LABEL_A = "Document A";
436
+ const LABEL_B = "Document B";
437
+ const fileA = document.getElementById("fileA").files[0];
438
+ const fileB = document.getElementById("fileB").files[0];
439
+
440
+ // Optional: Define regions to crop (limit comparison to specific areas)
441
+ const cropRegions = [
442
+ // { page: 1, x: 100, y: 150, width: 400, height: 200 }
443
+ ];
444
+ // Optional: Define regions to mask (ignore dynamic content like dates)
445
+ const maskRegions = [
446
+ // { page: 1, x: 50, y: 30, width: 200, height: 60 }
447
+ ];
448
+
449
+ console.log('Crop regions:', cropRegions);
450
+ console.log('Mask regions:', maskRegions);
451
+
452
+ if (!fileA || !fileB) {
453
+ alert("Select both PDFs first!");
454
+ return;
455
+ }
456
+
457
+ const arrayBufferA = await fileA.arrayBuffer();
458
+ const arrayBufferB = await fileB.arrayBuffer();
459
+
460
+ const pdfA = await pdfjsLib.getDocument({ data: arrayBufferA }).promise;
461
+ const pdfB = await pdfjsLib.getDocument({ data: arrayBufferB }).promise;
462
+
463
+ if (pdfA.numPages !== pdfB.numPages) {
464
+ alert(`Page mismatch: ${pdfA.numPages} vs ${pdfB.numPages}`);
465
+ return;
466
+ }
467
+
468
+ const canvasA = document.getElementById("canvasA");
469
+ const canvasB = document.getElementById("canvasB");
470
+ const canvasDiff = document.getElementById("canvasDiff");
471
+
472
+ const resultsDiv = document.getElementById("results");
473
+ resultsDiv.innerHTML = "";
474
+ // resultsDiv.innerHTML = "<h3>Diff on A vs Diff on B</h3>";
475
+
476
+ // const zip = new JSZip();
477
+ let totalDiffPixels = 0;
478
+
479
+ for (let i = 1; i <= pdfA.numPages; i++) {
480
+
481
+ const { words: wordsA } = await renderPageToCanvas(pdfA, i, canvasA);
482
+ const { words: wordsB } = await renderPageToCanvas(pdfB, i, canvasB);
483
+
484
+ const pageCrop = cropRegions.find(r => r.page === i);
485
+ const croppedWordsA = offsetWordBoxes(wordsA, pageCrop);
486
+ const croppedWordsB = offsetWordBoxes(wordsB, pageCrop);
487
+
488
+ const croppedA = applyCrop(canvasA, pageCrop);
489
+ const croppedB = applyCrop(canvasB, pageCrop);
490
+ const targetWidth = Math.max(croppedA.width, croppedB.width);
491
+ const targetHeight = Math.max(croppedA.height, croppedB.height);
492
+
493
+ const paddedA = padCanvas(croppedA, targetWidth, targetHeight);
494
+ const paddedB = padCanvas(croppedB, targetWidth, targetHeight);
495
+
496
+ const highlightCanvasB = document.createElement("canvas");
497
+ highlightCanvasB.width = targetWidth;
498
+ highlightCanvasB.height = targetHeight;
499
+ const highlightCtxB = highlightCanvasB.getContext("2d");
500
+
501
+ const imgA = canvasToImageData(paddedA);
502
+ const imgB = canvasToImageData(paddedB);
503
+
504
+ canvasDiff.width = targetWidth;
505
+ canvasDiff.height = targetHeight;
506
+
507
+ const ctxDiff = canvasDiff.getContext("2d");
508
+ ctxDiff.clearRect(0, 0, canvasDiff.width, canvasDiff.height);
509
+ const diffImage = ctxDiff.createImageData(imgA.width, imgA.height);
510
+
511
+ // Find best small translation to reduce reflow-induced diffs.
512
+ const best = findBestOffset(imgA, paddedB, imgA.width, imgA.height, COLOR_TOLERANCE);
513
+ const shiftedB = getShiftedImageData(paddedB, imgA.width, imgA.height, best.dx, best.dy);
514
+
515
+ const diffPixels = buildDiffImage(imgA, shiftedB, diffImage, COLOR_TOLERANCE);
516
+
517
+ // Apply masks to ignore specified regions
518
+ const pageMasks = maskRegions.filter(r => r.page === i);
519
+ applyMasks(diffImage, pageMasks);
520
+
521
+ // Expand mask so a tiny letter change highlights its word/line.
522
+ dilateDiffMask(diffImage, imgA.width, imgA.height, DILATION_RADIUS);
523
+
524
+ const boxes = extractDiffBoxes(diffImage, imgA.width, imgA.height, MIN_HIGHLIGHT_AREA);
525
+ totalDiffPixels += diffPixels;
526
+
527
+ const wordHighlightsA = mapDiffsToWordBoxes(boxes, croppedWordsA);
528
+ drawHighlightBoxes(ctxDiff, wordHighlightsA, 'red'); // LEFT = RED
529
+
530
+ const boxesForB = translateBoxes(boxes, -best.dx, -best.dy);
531
+ const wordHighlightsB = mapDiffsToWordBoxes(boxesForB, croppedWordsB);
532
+ drawHighlightBoxes(highlightCtxB, wordHighlightsB, 'green'); // RIGHT = GREEN
533
+ // Create overlays
534
+ const overlayOnA = overlayDiff(paddedA, canvasDiff);
535
+ const overlayOnB = overlayDiff(paddedB, highlightCanvasB);
536
+
537
+ // Save PURE diff to ZIP (still kept)
538
+ const diffDataUrl = canvasDiff.toDataURL("image/png");
539
+ const base64 = diffDataUrl.split(",")[1];
540
+ // zip.file(`diff_page_${i}.png`, base64, { base64: true });
541
+
542
+ // ---- UI Layout (2 columns only) ----
543
+ const title = document.createElement("h4");
544
+ title.innerText = `Page ${i}`;
545
+ // title.innerText = `Page ${i} — Diff pixels: ${diffPixels}`;
546
+
547
+ const row = document.createElement("div");
548
+ row.style.display = "grid";
549
+ row.style.gridTemplateColumns = "1fr 1fr";
550
+ row.style.gap = "15px";
551
+ row.style.marginBottom = "25px";
552
+ row.style.borderTop = "2px solid #ddd";
553
+ row.style.paddingTop = "15px";
554
+
555
+ function makeCol(labelText, canvas) {
556
+ const col = document.createElement("div");
557
+ const label = document.createElement("div");
558
+ label.innerHTML = `<b>${labelText}</b>`;
559
+
560
+ const img = document.createElement("img");
561
+ img.src = canvas.toDataURL("image/png");
562
+ img.style.width = "100%";
563
+ img.style.border = "1px solid #ccc";
564
+ img.style.imageRendering = "crisp-edges";
565
+ img.style.backgroundColor = "#fff";
566
+
567
+ col.appendChild(label);
568
+ col.appendChild(img);
569
+ return col;
570
+ }
571
+
572
+ resultsDiv.appendChild(title);
573
+ row.appendChild(makeCol(LABEL_A, overlayOnA));
574
+ row.appendChild(makeCol(LABEL_B, overlayOnB));
575
+
576
+ resultsDiv.appendChild(row);
577
+ }
578
+
579
+ const summary = document.createElement("h3");
580
+ summary.innerHTML =
581
+ ``;
582
+ // `Total pixel differences across all pages: ${totalDiffPixels}`;
583
+ resultsDiv.prepend(summary);
584
+
585
+ // downloadZip(zip, "pdf-diff-results.zip");
586
+ };
587
+
588
+ });
@@ -0,0 +1,39 @@
1
+ <!DOCTYPE html>
2
+ <html ng-app="pdfDiffApp">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>PDF Diff Viewer</title>
6
+
7
+ <!-- AngularJS -->
8
+ <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.0/angular.min.js"></script>
9
+
10
+ <!-- PDF.js browser build -->
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
12
+
13
+ <!-- Your Angular app -->
14
+ <script src="app.js"></script>
15
+ </head>
16
+
17
+
18
+ <body ng-controller="MainCtrl">
19
+
20
+ <h2>PDF Diff Viewer</h2>
21
+
22
+ <input type="file" id="fileA" accept="application/pdf">
23
+ <input type="file" id="fileB" accept="application/pdf">
24
+
25
+ <button ng-click="compare()">Compare PDFs</button>
26
+
27
+ <!-- <h3>Diff Results</h3> -->
28
+ <div id="results"></div>
29
+
30
+ <canvas id="canvasA" style="display:none"></canvas>
31
+ <canvas id="canvasB" style="display:none"></canvas>
32
+ <canvas id="canvasDiff"></canvas>
33
+
34
+ </body>
35
+ </html>
36
+
37
+ <script>
38
+ const PORT = process.env.PORT || 3000;
39
+ </script>
package/server.js ADDED
@@ -0,0 +1,20 @@
1
+ import express from "express";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ const app = express();
9
+
10
+ // Serve only your frontend
11
+ app.use(express.static(path.join(__dirname, "public")));
12
+
13
+ app.get("/", (req, res) => {
14
+ res.sendFile(path.join(__dirname, "public", "index.html"));
15
+ });
16
+
17
+ const PORT = process.env.PORT || 3000;
18
+ app.listen(PORT, () => {
19
+ console.log(`Server running on http://localhost:${PORT}`);
20
+ });