mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-09 09:04:18 +01:00
refactor: remove HTML report export and implement PDF export options in PDF comparison tool
- Deleted the exportCompareHtmlReport function and its related imports. - Introduced a dropdown menu for exporting comparison results as PDFs with multiple modes (split, alternating, left, right). - Updated the comparison logic to utilize caching for page models and comparison results. - Refactored rendering functions to improve code organization and maintainability. - Enhanced UI elements for better user experience during PDF export.
This commit is contained in:
1 parent
0fe94795cc
commit
d2a1450bc0
15 files changed
+953
-526
No files matched your search
@@ -0,0 +1,37 @@
|
||||
export const COMPARE_COLORS = {
|
||||
added: { r: 34, g: 197, b: 94 },
|
||||
removed: { r: 239, g: 68, b: 68 },
|
||||
modified: { r: 245, g: 158, b: 11 },
|
||||
} as const;
|
||||
|
||||
export const HIGHLIGHT_OPACITY = 0.28;
|
||||
|
||||
export const COMPARE_GEOMETRY = {
|
||||
LINE_TOLERANCE_FACTOR: 0.6,
|
||||
MIN_LINE_TOLERANCE: 4,
|
||||
FOCUS_REGION_PADDING: 40,
|
||||
FOCUS_REGION_MIN_WIDTH: 320,
|
||||
FOCUS_REGION_MIN_HEIGHT: 200,
|
||||
} as const;
|
||||
|
||||
export const COMPARE_RENDER = {
|
||||
OFFLINE_SCALE: 1.2,
|
||||
MAX_SCALE_OVERLAY: 2.5,
|
||||
MAX_SCALE_SIDE: 2.0,
|
||||
EXPORT_EXTRACT_SCALE: 1.0,
|
||||
SPLIT_GAP_PT: 2,
|
||||
} as const;
|
||||
|
||||
export const COMPARE_TEXT = {
|
||||
DEFAULT_CHAR_WIDTH: 1,
|
||||
DEFAULT_SPACE_WIDTH: 0.33,
|
||||
} as const;
|
||||
|
||||
export const VISUAL_DIFF = {
|
||||
PIXELMATCH_THRESHOLD: 0.12,
|
||||
ALPHA: 0.2,
|
||||
DIFF_COLOR: [239, 68, 68] as readonly [number, number, number],
|
||||
DIFF_COLOR_ALT: [34, 197, 94] as readonly [number, number, number],
|
||||
} as const;
|
||||
|
||||
export const COMPARE_CACHE_MAX_SIZE = 50;
|
||||
@@ -8,6 +8,8 @@ import type {
|
||||
CompareTextItem,
|
||||
CompareWordToken,
|
||||
} from '../types.ts';
|
||||
import { calculateBoundingRect } from './text-normalization.ts';
|
||||
import { COMPARE_GEOMETRY } from '../config.ts';
|
||||
|
||||
interface WordToken {
|
||||
word: string;
|
||||
@@ -86,7 +88,11 @@ function groupAdjacentRects(rects: CompareRectangle[]): CompareRectangle[] {
|
||||
const lastRect = prev[prev.length - 1];
|
||||
const curr = sorted[i];
|
||||
const sameLine =
|
||||
Math.abs(curr.y - lastRect.y) < Math.max(lastRect.height * 0.6, 4);
|
||||
Math.abs(curr.y - lastRect.y) <
|
||||
Math.max(
|
||||
lastRect.height * COMPARE_GEOMETRY.LINE_TOLERANCE_FACTOR,
|
||||
COMPARE_GEOMETRY.MIN_LINE_TOLERANCE
|
||||
);
|
||||
const close = curr.x <= lastRect.x + lastRect.width + lastRect.height * 2;
|
||||
|
||||
if (sameLine && close) {
|
||||
@@ -96,13 +102,7 @@ function groupAdjacentRects(rects: CompareRectangle[]): CompareRectangle[] {
|
||||
}
|
||||
}
|
||||
|
||||
return groups.map((group) => {
|
||||
const minX = Math.min(...group.map((r) => r.x));
|
||||
const minY = Math.min(...group.map((r) => r.y));
|
||||
const maxX = Math.max(...group.map((r) => r.x + r.width));
|
||||
const maxY = Math.max(...group.map((r) => r.y + r.height));
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
});
|
||||
return groups.map((group) => calculateBoundingRect(group));
|
||||
}
|
||||
|
||||
function collapseWords(words: WordToken[]) {
|
||||
|
||||
@@ -33,8 +33,10 @@ const textMeasurementCache: Map<string, number> | null = measurementContext
|
||||
: null;
|
||||
let lastMeasurementFont = '';
|
||||
|
||||
const DEFAULT_CHAR_WIDTH = 1;
|
||||
const DEFAULT_SPACE_WIDTH = 0.33;
|
||||
import { COMPARE_TEXT, COMPARE_GEOMETRY } from '../config.ts';
|
||||
|
||||
const DEFAULT_CHAR_WIDTH = COMPARE_TEXT.DEFAULT_CHAR_WIDTH;
|
||||
const DEFAULT_SPACE_WIDTH = COMPARE_TEXT.DEFAULT_SPACE_WIDTH;
|
||||
|
||||
function shouldJoinTokenWithPrevious(previous: string, current: string) {
|
||||
if (!previous) return false;
|
||||
@@ -261,8 +263,9 @@ function toRect(
|
||||
export function sortCompareTextItems(items: CompareTextItem[]) {
|
||||
return [...items].sort((left, right) => {
|
||||
const lineTolerance = Math.max(
|
||||
Math.min(left.rect.height, right.rect.height) * 0.6,
|
||||
4
|
||||
Math.min(left.rect.height, right.rect.height) *
|
||||
COMPARE_GEOMETRY.LINE_TOLERANCE_FACTOR,
|
||||
COMPARE_GEOMETRY.MIN_LINE_TOLERANCE
|
||||
);
|
||||
const topDiff = left.rect.y - right.rect.y;
|
||||
|
||||
@@ -450,8 +453,9 @@ export function mergeIntoLines(
|
||||
const anchor = currentLine[0];
|
||||
const curr = sortedItems[i];
|
||||
const lineTolerance = Math.max(
|
||||
Math.min(anchor.rect.height, curr.rect.height) * 0.6,
|
||||
4
|
||||
Math.min(anchor.rect.height, curr.rect.height) *
|
||||
COMPARE_GEOMETRY.LINE_TOLERANCE_FACTOR,
|
||||
COMPARE_GEOMETRY.MIN_LINE_TOLERANCE
|
||||
);
|
||||
|
||||
if (Math.abs(curr.rect.y - anchor.rect.y) <= lineTolerance) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { ComparePagePair, ComparePageSignature } from '../types.ts';
|
||||
|
||||
function tokenize(text: string) {
|
||||
return new Set(text.split(/\s+/).filter(Boolean));
|
||||
}
|
||||
import { tokenizeTextAsSet } from './text-normalization.ts';
|
||||
|
||||
function similarityScore(
|
||||
left: ComparePageSignature,
|
||||
@@ -16,8 +13,8 @@ function similarityScore(
|
||||
return 0.08;
|
||||
}
|
||||
|
||||
const leftTokens = tokenize(left.plainText);
|
||||
const rightTokens = tokenize(right.plainText);
|
||||
const leftTokens = tokenizeTextAsSet(left.plainText);
|
||||
const rightTokens = tokenizeTextAsSet(right.plainText);
|
||||
const union = new Set([...leftTokens, ...rightTokens]);
|
||||
let intersectionCount = 0;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CompareTextItem } from '../types.ts';
|
||||
import type { CompareRectangle, CompareTextItem } from '../types.ts';
|
||||
|
||||
export function normalizeCompareText(text: string) {
|
||||
return text
|
||||
@@ -62,3 +62,22 @@ export function isLowQualityExtractedText(text: string) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function tokenizeText(text: string): string[] {
|
||||
return text.split(/\s+/).filter(Boolean);
|
||||
}
|
||||
|
||||
export function tokenizeTextAsSet(text: string): Set<string> {
|
||||
return new Set(tokenizeText(text));
|
||||
}
|
||||
|
||||
export function calculateBoundingRect(
|
||||
rects: CompareRectangle[]
|
||||
): CompareRectangle {
|
||||
if (rects.length === 0) return { x: 0, y: 0, width: 0, height: 0 };
|
||||
const minX = Math.min(...rects.map((r) => r.x));
|
||||
const minY = Math.min(...rects.map((r) => r.y));
|
||||
const maxX = Math.max(...rects.map((r) => r.x + r.width));
|
||||
const maxY = Math.max(...rects.map((r) => r.y + r.height));
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import pixelmatch from 'pixelmatch';
|
||||
|
||||
import type { CompareVisualDiff } from '../types.ts';
|
||||
import { VISUAL_DIFF as VISUAL_DIFF_CONFIG } from '../config.ts';
|
||||
|
||||
type FocusRegion = {
|
||||
x: number;
|
||||
@@ -69,12 +70,16 @@ export function renderVisualDiff(
|
||||
width,
|
||||
height,
|
||||
{
|
||||
threshold: 0.12,
|
||||
threshold: VISUAL_DIFF_CONFIG.PIXELMATCH_THRESHOLD,
|
||||
includeAA: false,
|
||||
alpha: 0.2,
|
||||
alpha: VISUAL_DIFF_CONFIG.ALPHA,
|
||||
diffMask: false,
|
||||
diffColor: [239, 68, 68],
|
||||
diffColorAlt: [34, 197, 94],
|
||||
diffColor: [...VISUAL_DIFF_CONFIG.DIFF_COLOR] as [number, number, number],
|
||||
diffColorAlt: [...VISUAL_DIFF_CONFIG.DIFF_COLOR_ALT] as [
|
||||
number,
|
||||
number,
|
||||
number,
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
export class LRUCache<K, V> {
|
||||
private map = new Map<K, V>();
|
||||
private maxSize: number;
|
||||
|
||||
constructor(maxSize: number) {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
get(key: K): V | undefined {
|
||||
const value = this.map.get(key);
|
||||
if (value !== undefined) {
|
||||
this.map.delete(key);
|
||||
this.map.set(key, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key: K, value: V) {
|
||||
this.map.delete(key);
|
||||
this.map.set(key, value);
|
||||
if (this.map.size > this.maxSize) {
|
||||
const oldest = this.map.keys().next().value;
|
||||
if (oldest !== undefined) this.map.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
has(key: K): boolean {
|
||||
return this.map.has(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.map.clear();
|
||||
}
|
||||
|
||||
get size(): number {
|
||||
return this.map.size;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import type { ComparePagePair, ComparePageResult } from '../types.ts';
|
||||
|
||||
function escapeHtml(text: string) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function buildCompareReport(
|
||||
fileName1: string,
|
||||
fileName2: string,
|
||||
pairs: ComparePagePair[],
|
||||
results: ComparePageResult[]
|
||||
) {
|
||||
const totals = results.reduce(
|
||||
(summary, result) => {
|
||||
summary.added += result.summary.added;
|
||||
summary.removed += result.summary.removed;
|
||||
summary.modified += result.summary.modified;
|
||||
return summary;
|
||||
},
|
||||
{ added: 0, removed: 0, modified: 0 }
|
||||
);
|
||||
|
||||
const rows = results
|
||||
.map((result, index) => {
|
||||
const pair = pairs[index];
|
||||
const changes = result.changes
|
||||
.map(
|
||||
(change) =>
|
||||
`<li><strong>${escapeHtml(change.type)}</strong>: ${escapeHtml(change.description)}</li>`
|
||||
)
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<section class="pair-card">
|
||||
<h2>Comparison ${pair?.pairIndex || index + 1}</h2>
|
||||
<p class="meta">PDF 1 page: ${pair?.leftPageNumber ?? 'none'} | PDF 2 page: ${pair?.rightPageNumber ?? 'none'} | Confidence: ${((pair?.confidence || 0) * 100).toFixed(0)}%</p>
|
||||
<p class="meta">Status: ${escapeHtml(result.status)}${result.usedOcr ? ' | OCR used' : ''}</p>
|
||||
<p class="meta">Added: ${result.summary.added} | Removed: ${result.summary.removed} | Modified: ${result.summary.modified}</p>
|
||||
<ul>${changes || '<li>No semantic changes detected.</li>'}</ul>
|
||||
</section>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Compare report</title>
|
||||
<style>
|
||||
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 0; padding: 2rem; background: #111827; color: #e5e7eb; }
|
||||
.summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin: 1.5rem 0; }
|
||||
.card, .pair-card { background: #1f2937; border: 1px solid #374151; border-radius: 12px; padding: 1rem 1.25rem; }
|
||||
.pair-card { margin-bottom: 1rem; }
|
||||
.meta { color: #9ca3af; font-size: 0.95rem; }
|
||||
h1, h2 { margin: 0 0 0.75rem 0; }
|
||||
ul { margin: 0.75rem 0 0 1.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>PDF Compare Report</h1>
|
||||
<p class="meta">PDF 1: ${escapeHtml(fileName1)} | PDF 2: ${escapeHtml(fileName2)}</p>
|
||||
<div class="summary">
|
||||
<div class="card"><div class="meta">Added</div><div>${totals.added}</div></div>
|
||||
<div class="card"><div class="meta">Removed</div><div>${totals.removed}</div></div>
|
||||
<div class="card"><div class="meta">Modified</div><div>${totals.modified}</div></div>
|
||||
</div>
|
||||
${rows}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { PDFDocument, rgb } from 'pdf-lib';
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
import type {
|
||||
ComparePagePair,
|
||||
CompareTextChange,
|
||||
ComparePdfExportMode,
|
||||
} from '../types.ts';
|
||||
import { extractPageModel } from '../engine/extract-page-model.ts';
|
||||
import { comparePageModels } from '../engine/compare-page-models.ts';
|
||||
import {
|
||||
COMPARE_COLORS,
|
||||
HIGHLIGHT_OPACITY,
|
||||
COMPARE_RENDER,
|
||||
} from '../config.ts';
|
||||
import { downloadFile } from '../../utils/helpers.ts';
|
||||
|
||||
const HIGHLIGHT_COLORS: Record<
|
||||
string,
|
||||
{ r: number; g: number; b: number; opacity: number }
|
||||
> = {
|
||||
added: {
|
||||
r: COMPARE_COLORS.added.r / 255,
|
||||
g: COMPARE_COLORS.added.g / 255,
|
||||
b: COMPARE_COLORS.added.b / 255,
|
||||
opacity: HIGHLIGHT_OPACITY,
|
||||
},
|
||||
removed: {
|
||||
r: COMPARE_COLORS.removed.r / 255,
|
||||
g: COMPARE_COLORS.removed.g / 255,
|
||||
b: COMPARE_COLORS.removed.b / 255,
|
||||
opacity: HIGHLIGHT_OPACITY,
|
||||
},
|
||||
'page-removed': {
|
||||
r: COMPARE_COLORS.removed.r / 255,
|
||||
g: COMPARE_COLORS.removed.g / 255,
|
||||
b: COMPARE_COLORS.removed.b / 255,
|
||||
opacity: HIGHLIGHT_OPACITY,
|
||||
},
|
||||
modified: {
|
||||
r: COMPARE_COLORS.modified.r / 255,
|
||||
g: COMPARE_COLORS.modified.g / 255,
|
||||
b: COMPARE_COLORS.modified.b / 255,
|
||||
opacity: HIGHLIGHT_OPACITY,
|
||||
},
|
||||
};
|
||||
|
||||
const EXTRACT_SCALE = COMPARE_RENDER.EXPORT_EXTRACT_SCALE;
|
||||
|
||||
function drawHighlights(
|
||||
page: ReturnType<PDFDocument['getPage']>,
|
||||
pageHeight: number,
|
||||
changes: CompareTextChange[],
|
||||
side: 'before' | 'after'
|
||||
) {
|
||||
for (const change of changes) {
|
||||
const rects = side === 'before' ? change.beforeRects : change.afterRects;
|
||||
const color = HIGHLIGHT_COLORS[change.type];
|
||||
if (!color) continue;
|
||||
for (const rect of rects) {
|
||||
page.drawRectangle({
|
||||
x: rect.x / EXTRACT_SCALE,
|
||||
y: pageHeight - rect.y / EXTRACT_SCALE - rect.height / EXTRACT_SCALE,
|
||||
width: rect.width / EXTRACT_SCALE,
|
||||
height: rect.height / EXTRACT_SCALE,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
opacity: color.opacity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportComparePdf(
|
||||
mode: ComparePdfExportMode,
|
||||
pdfDoc1: pdfjsLib.PDFDocumentProxy | null,
|
||||
pdfDoc2: pdfjsLib.PDFDocumentProxy | null,
|
||||
pairs: ComparePagePair[],
|
||||
onProgress?: (message: string, percent: number) => void
|
||||
) {
|
||||
if (!pdfDoc1 && !pdfDoc2) {
|
||||
throw new Error('At least one PDF document is required for export.');
|
||||
}
|
||||
if (!pairs || pairs.length === 0) {
|
||||
throw new Error('No page pairs to export.');
|
||||
}
|
||||
|
||||
const outDoc = await PDFDocument.create();
|
||||
|
||||
const [bytes1, bytes2] = await Promise.all([
|
||||
pdfDoc1?.getData(),
|
||||
pdfDoc2?.getData(),
|
||||
]);
|
||||
|
||||
const [libDoc1, libDoc2] = await Promise.all([
|
||||
bytes1 ? PDFDocument.load(bytes1, { ignoreEncryption: true }) : null,
|
||||
bytes2 ? PDFDocument.load(bytes2, { ignoreEncryption: true }) : null,
|
||||
]);
|
||||
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const pair = pairs[i];
|
||||
onProgress?.(
|
||||
`Rendering page ${i + 1} of ${pairs.length}...`,
|
||||
Math.round(((i + 1) / pairs.length) * 100)
|
||||
);
|
||||
|
||||
const leftPdjsPage =
|
||||
pair.leftPageNumber && pdfDoc1
|
||||
? await pdfDoc1.getPage(pair.leftPageNumber)
|
||||
: null;
|
||||
const rightPdjsPage =
|
||||
pair.rightPageNumber && pdfDoc2
|
||||
? await pdfDoc2.getPage(pair.rightPageNumber)
|
||||
: null;
|
||||
|
||||
const leftModel = leftPdjsPage
|
||||
? await extractPageModel(
|
||||
leftPdjsPage,
|
||||
leftPdjsPage.getViewport({ scale: EXTRACT_SCALE })
|
||||
)
|
||||
: null;
|
||||
const rightModel = rightPdjsPage
|
||||
? await extractPageModel(
|
||||
rightPdjsPage,
|
||||
rightPdjsPage.getViewport({ scale: EXTRACT_SCALE })
|
||||
)
|
||||
: null;
|
||||
|
||||
const comparison = comparePageModels(leftModel, rightModel);
|
||||
const changes = comparison.changes;
|
||||
|
||||
if (mode === 'split') {
|
||||
const refPage = leftPdjsPage || rightPdjsPage;
|
||||
const vp = refPage!.getViewport({ scale: 1.0 });
|
||||
const gap = COMPARE_RENDER.SPLIT_GAP_PT;
|
||||
const totalW = vp.width * 2 + gap;
|
||||
const outPage = outDoc.addPage([totalW, vp.height]);
|
||||
|
||||
if (pair.leftPageNumber && libDoc1) {
|
||||
const [copied] = await outDoc.copyPages(libDoc1, [
|
||||
pair.leftPageNumber - 1,
|
||||
]);
|
||||
const embedded = await outDoc.embedPage(copied);
|
||||
outPage.drawPage(embedded, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: vp.width,
|
||||
height: vp.height,
|
||||
});
|
||||
}
|
||||
if (pair.rightPageNumber && libDoc2) {
|
||||
const [copied] = await outDoc.copyPages(libDoc2, [
|
||||
pair.rightPageNumber - 1,
|
||||
]);
|
||||
const embedded = await outDoc.embedPage(copied);
|
||||
outPage.drawPage(embedded, {
|
||||
x: vp.width + gap,
|
||||
y: 0,
|
||||
width: vp.width,
|
||||
height: vp.height,
|
||||
});
|
||||
}
|
||||
|
||||
if (changes.length) {
|
||||
for (const change of changes) {
|
||||
const color = HIGHLIGHT_COLORS[change.type];
|
||||
if (!color) continue;
|
||||
for (const rect of change.beforeRects) {
|
||||
outPage.drawRectangle({
|
||||
x: rect.x / EXTRACT_SCALE,
|
||||
y:
|
||||
vp.height -
|
||||
rect.y / EXTRACT_SCALE -
|
||||
rect.height / EXTRACT_SCALE,
|
||||
width: rect.width / EXTRACT_SCALE,
|
||||
height: rect.height / EXTRACT_SCALE,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
opacity: color.opacity,
|
||||
});
|
||||
}
|
||||
for (const rect of change.afterRects) {
|
||||
outPage.drawRectangle({
|
||||
x: vp.width + gap + rect.x / EXTRACT_SCALE,
|
||||
y:
|
||||
vp.height -
|
||||
rect.y / EXTRACT_SCALE -
|
||||
rect.height / EXTRACT_SCALE,
|
||||
width: rect.width / EXTRACT_SCALE,
|
||||
height: rect.height / EXTRACT_SCALE,
|
||||
color: rgb(color.r, color.g, color.b),
|
||||
opacity: color.opacity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mode === 'alternating') {
|
||||
if (pair.leftPageNumber && libDoc1) {
|
||||
const [copied] = await outDoc.copyPages(libDoc1, [
|
||||
pair.leftPageNumber - 1,
|
||||
]);
|
||||
const embedded = outDoc.addPage(copied);
|
||||
const { height } = embedded.getSize();
|
||||
if (changes.length) drawHighlights(embedded, height, changes, 'before');
|
||||
}
|
||||
if (pair.rightPageNumber && libDoc2) {
|
||||
const [copied] = await outDoc.copyPages(libDoc2, [
|
||||
pair.rightPageNumber - 1,
|
||||
]);
|
||||
const embedded = outDoc.addPage(copied);
|
||||
const { height } = embedded.getSize();
|
||||
if (changes.length) drawHighlights(embedded, height, changes, 'after');
|
||||
}
|
||||
} else if (mode === 'left') {
|
||||
if (pair.leftPageNumber && libDoc1) {
|
||||
const [copied] = await outDoc.copyPages(libDoc1, [
|
||||
pair.leftPageNumber - 1,
|
||||
]);
|
||||
const embedded = outDoc.addPage(copied);
|
||||
const { height } = embedded.getSize();
|
||||
if (changes.length) drawHighlights(embedded, height, changes, 'before');
|
||||
}
|
||||
} else {
|
||||
if (pair.rightPageNumber && libDoc2) {
|
||||
const [copied] = await outDoc.copyPages(libDoc2, [
|
||||
pair.rightPageNumber - 1,
|
||||
]);
|
||||
const embedded = outDoc.addPage(copied);
|
||||
const { height } = embedded.getSize();
|
||||
if (changes.length) drawHighlights(embedded, height, changes, 'after');
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
}
|
||||
|
||||
const pdfBytes = await outDoc.save();
|
||||
const blob = new Blob([pdfBytes.buffer as ArrayBuffer], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
downloadFile(blob, 'bentopdf-compare-export.pdf');
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { buildCompareReport } from './build-report.ts';
|
||||
import type { ComparePagePair, ComparePageResult } from '../types.ts';
|
||||
|
||||
export function exportCompareHtmlReport(
|
||||
fileName1: string,
|
||||
fileName2: string,
|
||||
pairs: ComparePagePair[],
|
||||
results: ComparePageResult[]
|
||||
) {
|
||||
const html = buildCompareReport(fileName1, fileName2, pairs, results);
|
||||
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.download = 'bentopdf-compare-report.html';
|
||||
anchor.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -1,7 +1,40 @@
|
||||
import type * as pdfjsLib from 'pdfjs-dist';
|
||||
import type { LRUCache } from './lru-cache.ts';
|
||||
|
||||
export type CompareViewMode = 'overlay' | 'side-by-side';
|
||||
|
||||
export type ComparePdfExportMode = 'split' | 'alternating' | 'left' | 'right';
|
||||
|
||||
export interface RenderedPage {
|
||||
model: ComparePageModel | null;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface ComparisonPageLoad {
|
||||
model: ComparePageModel | null;
|
||||
exists: boolean;
|
||||
}
|
||||
|
||||
export interface DiffFocusRegion {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface CompareCaches {
|
||||
pageModelCache: LRUCache<string, ComparePageModel>;
|
||||
comparisonCache: LRUCache<string, ComparePageResult>;
|
||||
comparisonResultsCache: LRUCache<number, ComparePageResult>;
|
||||
}
|
||||
|
||||
export interface CompareRenderContext {
|
||||
useOcr: boolean;
|
||||
ocrLanguage: string;
|
||||
viewMode: CompareViewMode;
|
||||
showLoader: (message: string, percent?: number) => void;
|
||||
}
|
||||
|
||||
export interface CompareRectangle {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
Reference in new issue
Block a user