refactor: update PDF comparison types and enhance UI for better usability
- Refactored CompareState to import from a centralized type definition. - Enhanced the compare-pdfs.html layout with improved styles for overlay and side-by-side modes. - Added new CSS styles for various UI components including panels, buttons, and highlights. - Implemented a new sidebar for displaying change summaries and filters. - Created unit tests for text comparison logic, including diffing text runs and page pairing. - Added tests for text normalization functions to ensure proper handling of punctuation and character normalization.
This commit is contained in:
134
src/js/compare/engine/visual-diff.ts
Normal file
134
src/js/compare/engine/visual-diff.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import pixelmatch from 'pixelmatch';
|
||||
|
||||
import type { CompareVisualDiff } from '../types.ts';
|
||||
|
||||
type FocusRegion = {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
function createCanvas(width: number, height: number) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function drawNormalized(
|
||||
sourceCanvas: HTMLCanvasElement,
|
||||
targetCanvas: HTMLCanvasElement
|
||||
) {
|
||||
const context = targetCanvas.getContext('2d');
|
||||
if (!context) {
|
||||
throw new Error('Could not create comparison canvas context.');
|
||||
}
|
||||
|
||||
context.fillStyle = '#ffffff';
|
||||
context.fillRect(0, 0, targetCanvas.width, targetCanvas.height);
|
||||
|
||||
const offsetX = Math.floor((targetCanvas.width - sourceCanvas.width) / 2);
|
||||
const offsetY = Math.floor((targetCanvas.height - sourceCanvas.height) / 2);
|
||||
context.drawImage(sourceCanvas, offsetX, offsetY);
|
||||
}
|
||||
|
||||
export function renderVisualDiff(
|
||||
canvas1: HTMLCanvasElement,
|
||||
canvas2: HTMLCanvasElement,
|
||||
outputCanvas: HTMLCanvasElement,
|
||||
focusRegion?: FocusRegion
|
||||
): CompareVisualDiff {
|
||||
const width = Math.max(canvas1.width, canvas2.width, 1);
|
||||
const height = Math.max(canvas1.height, canvas2.height, 1);
|
||||
const normalizedCanvas1 = createCanvas(width, height);
|
||||
const normalizedCanvas2 = createCanvas(width, height);
|
||||
|
||||
drawNormalized(canvas1, normalizedCanvas1);
|
||||
drawNormalized(canvas2, normalizedCanvas2);
|
||||
|
||||
outputCanvas.width = width;
|
||||
outputCanvas.height = height;
|
||||
|
||||
const context1 = normalizedCanvas1.getContext('2d');
|
||||
const context2 = normalizedCanvas2.getContext('2d');
|
||||
const outputContext = outputCanvas.getContext('2d');
|
||||
|
||||
if (!context1 || !context2 || !outputContext) {
|
||||
throw new Error('Could not create visual diff context.');
|
||||
}
|
||||
|
||||
const image1 = context1.getImageData(0, 0, width, height);
|
||||
const image2 = context2.getImageData(0, 0, width, height);
|
||||
const diffImage = outputContext.createImageData(width, height);
|
||||
|
||||
const mismatchPixels = pixelmatch(
|
||||
image1.data,
|
||||
image2.data,
|
||||
diffImage.data,
|
||||
width,
|
||||
height,
|
||||
{
|
||||
threshold: 0.12,
|
||||
includeAA: false,
|
||||
alpha: 0.2,
|
||||
diffMask: false,
|
||||
diffColor: [239, 68, 68],
|
||||
diffColorAlt: [34, 197, 94],
|
||||
}
|
||||
);
|
||||
|
||||
const overlayCanvas = createCanvas(width, height);
|
||||
const overlayContext = overlayCanvas.getContext('2d');
|
||||
|
||||
if (!overlayContext) {
|
||||
throw new Error('Could not create visual diff overlay context.');
|
||||
}
|
||||
|
||||
overlayContext.putImageData(diffImage, 0, 0);
|
||||
|
||||
const region = focusRegion
|
||||
? {
|
||||
x: Math.max(Math.floor(focusRegion.x), 0),
|
||||
y: Math.max(Math.floor(focusRegion.y), 0),
|
||||
width: Math.min(Math.ceil(focusRegion.width), width),
|
||||
height: Math.min(Math.ceil(focusRegion.height), height),
|
||||
}
|
||||
: { x: 0, y: 0, width, height };
|
||||
|
||||
outputCanvas.width = Math.max(region.width, 1);
|
||||
outputCanvas.height = Math.max(region.height, 1);
|
||||
|
||||
outputContext.fillStyle = '#ffffff';
|
||||
outputContext.fillRect(0, 0, outputCanvas.width, outputCanvas.height);
|
||||
outputContext.drawImage(
|
||||
normalizedCanvas2,
|
||||
region.x,
|
||||
region.y,
|
||||
region.width,
|
||||
region.height,
|
||||
0,
|
||||
0,
|
||||
outputCanvas.width,
|
||||
outputCanvas.height
|
||||
);
|
||||
outputContext.globalAlpha = 0.9;
|
||||
outputContext.drawImage(
|
||||
overlayCanvas,
|
||||
region.x,
|
||||
region.y,
|
||||
region.width,
|
||||
region.height,
|
||||
0,
|
||||
0,
|
||||
outputCanvas.width,
|
||||
outputCanvas.height
|
||||
);
|
||||
outputContext.globalAlpha = 1;
|
||||
|
||||
return {
|
||||
mismatchPixels,
|
||||
mismatchRatio: mismatchPixels / Math.max(width * height, 1),
|
||||
hasDiff: mismatchPixels > 0,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user