mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-11-10 17:14:34 +01:00
feat: enhance PDF comparison with new change types and zoom functionality
- Added support for 'moved' and 'style-changed' change types in PDF comparison. - Implemented category filters for changes, allowing users to filter by text, images, headers, annotations, formatting, and background. - Introduced zoom functionality with buttons for zooming in, out, and resetting to default. - Updated UI to reflect new change types and categories, including visual indicators for moved and style-changed items. - Enhanced summary display to include counts for moved and style-changed changes. - Refactored rendering logic to accommodate zoom levels and improve performance. - Added tests for new change detection features and category assignments.
This commit is contained in:
1 parent
89d7cd8e3d
commit
5232102ac0
31 files changed
+2503
-183
No files matched your search
+137
-22
@@ -3,6 +3,9 @@ import type {
|
||||
ComparePageModel,
|
||||
ComparePagePair,
|
||||
ComparePageResult,
|
||||
CompareRectangle,
|
||||
CompareWordToken,
|
||||
CompareTextItem,
|
||||
RenderedPage,
|
||||
ComparisonPageLoad,
|
||||
DiffFocusRegion,
|
||||
@@ -10,7 +13,7 @@ import type {
|
||||
CompareRenderContext,
|
||||
} from '../compare/types.ts';
|
||||
import { extractPageModel } from '../compare/engine/extract-page-model.ts';
|
||||
import { comparePageModels } from '../compare/engine/compare-page-models.ts';
|
||||
import { comparePageModelsAsync } from '../compare/engine/compare-page-models.ts';
|
||||
import { renderVisualDiff } from '../compare/engine/visual-diff.ts';
|
||||
import { recognizePageCanvas } from '../compare/engine/ocr-page.ts';
|
||||
import { isLowQualityExtractedText } from '../compare/engine/text-normalization.ts';
|
||||
@@ -48,7 +51,8 @@ export function hidePlaceholder(placeholderId: string) {
|
||||
export function getRenderScale(
|
||||
page: pdfjsLib.PDFPageProxy,
|
||||
container: HTMLElement,
|
||||
viewMode: 'overlay' | 'side-by-side'
|
||||
viewMode: 'overlay' | 'side-by-side',
|
||||
zoomLevel = 1.0
|
||||
) {
|
||||
const baseViewport = page.getViewport({ scale: 1.0 });
|
||||
const availableWidth = Math.max(
|
||||
@@ -61,7 +65,8 @@ export function getRenderScale(
|
||||
? COMPARE_RENDER.MAX_SCALE_OVERLAY
|
||||
: COMPARE_RENDER.MAX_SCALE_SIDE;
|
||||
|
||||
return Math.min(Math.max(fitScale, 1.0), maxScale);
|
||||
const baseScale = Math.min(Math.max(fitScale, 1.0), maxScale);
|
||||
return baseScale * zoomLevel;
|
||||
}
|
||||
|
||||
export function getPageModelCacheKey(
|
||||
@@ -76,6 +81,72 @@ function shouldUseOcrForModel(model: ComparePageModel) {
|
||||
return !model.hasText || isLowQualityExtractedText(model.plainText);
|
||||
}
|
||||
|
||||
function rescaleRect(
|
||||
rect: CompareRectangle,
|
||||
scaleX: number,
|
||||
scaleY: number
|
||||
): CompareRectangle {
|
||||
return {
|
||||
x: rect.x * scaleX,
|
||||
y: rect.y * scaleY,
|
||||
width: rect.width * scaleX,
|
||||
height: rect.height * scaleY,
|
||||
};
|
||||
}
|
||||
|
||||
function rescaleWordToken(
|
||||
token: CompareWordToken,
|
||||
scaleX: number,
|
||||
scaleY: number
|
||||
): CompareWordToken {
|
||||
return {
|
||||
...token,
|
||||
rect: rescaleRect(token.rect, scaleX, scaleY),
|
||||
};
|
||||
}
|
||||
|
||||
function rescaleTextItem(
|
||||
item: CompareTextItem,
|
||||
scaleX: number,
|
||||
scaleY: number
|
||||
): CompareTextItem {
|
||||
return {
|
||||
...item,
|
||||
rect: rescaleRect(item.rect, scaleX, scaleY),
|
||||
charMap: item.charMap?.map((c) => ({
|
||||
x: c.x * scaleX,
|
||||
width: c.width * scaleX,
|
||||
})),
|
||||
wordTokens: item.wordTokens?.map((t) =>
|
||||
rescaleWordToken(t, scaleX, scaleY)
|
||||
),
|
||||
fragments: item.fragments?.map((f) => rescaleTextItem(f, scaleX, scaleY)),
|
||||
};
|
||||
}
|
||||
|
||||
function rescalePageModel(
|
||||
model: ComparePageModel,
|
||||
cachedWidth: number,
|
||||
cachedHeight: number,
|
||||
targetWidth: number,
|
||||
targetHeight: number
|
||||
): ComparePageModel {
|
||||
const scaleX = targetWidth / Math.max(cachedWidth, 1);
|
||||
const scaleY = targetHeight / Math.max(cachedHeight, 1);
|
||||
return {
|
||||
...model,
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
textItems: model.textItems.map((item) =>
|
||||
rescaleTextItem(item, scaleX, scaleY)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function getOcrCacheKey(side: string, pageNum: number) {
|
||||
return `${side}-${pageNum}`;
|
||||
}
|
||||
|
||||
export function buildDiffFocusRegion(
|
||||
comparison: ComparePageResult,
|
||||
leftCanvas: HTMLCanvasElement,
|
||||
@@ -164,7 +235,12 @@ export async function renderPage(
|
||||
|
||||
const page = await pdfDoc.getPage(pageNum);
|
||||
|
||||
const targetScale = getRenderScale(page, container, ctx.viewMode);
|
||||
const targetScale = getRenderScale(
|
||||
page,
|
||||
container,
|
||||
ctx.viewMode,
|
||||
ctx.zoomLevel
|
||||
);
|
||||
const scaledViewport = page.getViewport({ scale: targetScale });
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const hiResViewport = page.getViewport({ scale: targetScale * dpr });
|
||||
@@ -192,18 +268,36 @@ export async function renderPage(
|
||||
let finalModel = model;
|
||||
|
||||
if (!cachedModel && ctx.useOcr && shouldUseOcrForModel(model)) {
|
||||
ctx.showLoader(`Running OCR on page ${pageNum}...`);
|
||||
const ocrModel = await recognizePageCanvas(
|
||||
canvas,
|
||||
ctx.ocrLanguage,
|
||||
function (status, progress) {
|
||||
ctx.showLoader(`OCR: ${status}`, progress * 100);
|
||||
}
|
||||
);
|
||||
finalModel = {
|
||||
...ocrModel,
|
||||
pageNumber: pageNum,
|
||||
};
|
||||
const ocrKey = getOcrCacheKey(cacheKeyPrefix, pageNum);
|
||||
const cachedOcr = caches.ocrModelCache.get(ocrKey);
|
||||
if (cachedOcr) {
|
||||
finalModel = rescalePageModel(
|
||||
cachedOcr.model,
|
||||
cachedOcr.width,
|
||||
cachedOcr.height,
|
||||
scaledViewport.width,
|
||||
scaledViewport.height
|
||||
);
|
||||
finalModel.pageNumber = pageNum;
|
||||
} else {
|
||||
ctx.showLoader(`Running OCR on page ${pageNum}...`);
|
||||
const ocrModel = await recognizePageCanvas(
|
||||
canvas,
|
||||
ctx.ocrLanguage,
|
||||
function (status, progress) {
|
||||
ctx.showLoader(`OCR: ${status}`, progress * 100);
|
||||
}
|
||||
);
|
||||
finalModel = {
|
||||
...ocrModel,
|
||||
pageNumber: pageNum,
|
||||
};
|
||||
caches.ocrModelCache.set(ocrKey, {
|
||||
model: finalModel,
|
||||
width: scaledViewport.width,
|
||||
height: scaledViewport.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
caches.pageModelCache.set(cacheKey, finalModel);
|
||||
@@ -276,11 +370,29 @@ export async function loadComparisonPage(
|
||||
|
||||
let finalModel = extractedModel;
|
||||
if (ctx.useOcr && shouldUseOcrForModel(extractedModel)) {
|
||||
const ocrModel = await recognizePageCanvas(canvas, ctx.ocrLanguage);
|
||||
finalModel = {
|
||||
...ocrModel,
|
||||
pageNumber: pageNum,
|
||||
};
|
||||
const ocrKey = getOcrCacheKey(side, pageNum);
|
||||
const cachedOcr = caches.ocrModelCache.get(ocrKey);
|
||||
if (cachedOcr) {
|
||||
finalModel = rescalePageModel(
|
||||
cachedOcr.model,
|
||||
cachedOcr.width,
|
||||
cachedOcr.height,
|
||||
viewport.width,
|
||||
viewport.height
|
||||
);
|
||||
finalModel.pageNumber = pageNum;
|
||||
} else {
|
||||
const ocrModel = await recognizePageCanvas(canvas, ctx.ocrLanguage);
|
||||
finalModel = {
|
||||
...ocrModel,
|
||||
pageNumber: pageNum,
|
||||
};
|
||||
caches.ocrModelCache.set(ocrKey, {
|
||||
model: finalModel,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = 0;
|
||||
@@ -330,7 +442,10 @@ export async function computeComparisonForPair(
|
||||
ctx
|
||||
);
|
||||
|
||||
const comparison = comparePageModels(leftPage.model, rightPage.model);
|
||||
const comparison = await comparePageModelsAsync(
|
||||
leftPage.model,
|
||||
rightPage.model
|
||||
);
|
||||
comparison.confidence = pair.confidence;
|
||||
|
||||
if (
|
||||
|
||||
Reference in new issue
Block a user