diff --git a/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte b/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte
index 6f6caad0fc..2ef9ec1054 100644
--- a/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte
+++ b/web/src/lib/components/asset-viewer/ocr-bounding-box.svelte
@@ -1,6 +1,6 @@
{ocrBox.text}
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index 3e609ff130..38471f8a41 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -15,7 +15,7 @@
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
import { getAssetUrl, targetImageSize as getTargetImageSize, handlePromiseError } from '$lib/utils';
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
- import { type ContentMetrics, getContentMetrics } from '$lib/utils/container-utils';
+ import { type ContentMetrics, getNaturalSize, scaleToFit } from '$lib/utils/container-utils';
import { handleError } from '$lib/utils/handle-error';
import { getOcrBoundingBoxes } from '$lib/utils/ocr-utils';
import { getBoundingBox } from '$lib/utils/people-utils';
@@ -74,12 +74,16 @@
return { contentWidth: 0, contentHeight: 0, offsetX: 0, offsetY: 0 };
}
- const { contentWidth, contentHeight, offsetX, offsetY } = getContentMetrics(assetViewerManager.imgRef);
+ const natural = getNaturalSize(assetViewerManager.imgRef);
+ const client = { width: containerWidth, height: containerHeight };
+ const scaled = scaleToFit(natural, client);
+ const offsetX = (client.width - scaled.width) / 2;
+ const offsetY = (client.height - scaled.height) / 2;
const { currentZoom, currentPositionX, currentPositionY } = assetViewerManager.zoomState;
return {
- contentWidth: contentWidth * currentZoom,
- contentHeight: contentHeight * currentZoom,
+ contentWidth: scaled.width * currentZoom,
+ contentHeight: scaled.height * currentZoom,
offsetX: offsetX * currentZoom + currentPositionX,
offsetY: offsetY * currentZoom + currentPositionY,
};
diff --git a/web/src/lib/utils/ocr-utils.ts b/web/src/lib/utils/ocr-utils.ts
index 3da36cf57a..a97f82be5d 100644
--- a/web/src/lib/utils/ocr-utils.ts
+++ b/web/src/lib/utils/ocr-utils.ts
@@ -1,5 +1,6 @@
import type { OcrBoundingBox } from '$lib/stores/ocr.svelte';
import type { ContentMetrics } from '$lib/utils/container-utils';
+import { clamp } from 'lodash-es';
export type Point = {
x: number;
@@ -55,6 +56,54 @@ export const calculateBoundingBoxMatrix = (points: Point[]): { matrix: number[];
return { matrix, width, height };
};
+const HORIZONTAL_PADDING = 16;
+const VERTICAL_PADDING = 8;
+const REFERENCE_FONT_SIZE = 100;
+const MIN_FONT_SIZE = 8;
+const MAX_FONT_SIZE = 96;
+const REFERENCE_FONT = `${REFERENCE_FONT_SIZE}px 'GoogleSans', sans-serif`;
+
+let sharedCanvasContext: CanvasRenderingContext2D | null = null;
+
+const getCanvasContext = (): CanvasRenderingContext2D | null => {
+ if (sharedCanvasContext !== null) {
+ return sharedCanvasContext;
+ }
+ if (typeof document === 'undefined') {
+ return null;
+ }
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ if (!context) {
+ return null;
+ }
+ sharedCanvasContext = context;
+ return sharedCanvasContext;
+};
+
+export const calculateFittedFontSize = (text: string, boxWidth: number, boxHeight: number): number => {
+ const availableWidth = boxWidth - HORIZONTAL_PADDING;
+ const availableHeight = boxHeight - VERTICAL_PADDING;
+
+ const context = getCanvasContext();
+ if (!context) {
+ return clamp((1.4 * availableWidth) / text.length, MIN_FONT_SIZE, MAX_FONT_SIZE);
+ }
+
+ // Unsupported in Safari iOS <16.6; falls back to default canvas font, giving less accurate but functional sizing
+ // eslint-disable-next-line tscompat/tscompat
+ context.font = REFERENCE_FONT;
+
+ const metrics = context.measureText(text);
+ const measuredWidth = metrics.width;
+ const measuredHeight = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
+
+ const scaleFromWidth = (availableWidth / measuredWidth) * REFERENCE_FONT_SIZE;
+ const scaleFromHeight = (availableHeight / measuredHeight) * REFERENCE_FONT_SIZE;
+
+ return clamp(Math.min(scaleFromWidth, scaleFromHeight), MIN_FONT_SIZE, MAX_FONT_SIZE);
+};
+
export const getOcrBoundingBoxes = (ocrData: OcrBoundingBox[], metrics: ContentMetrics): OcrBox[] => {
const boxes: OcrBox[] = [];
for (const ocr of ocrData) {
@@ -76,5 +125,14 @@ export const getOcrBoundingBoxes = (ocrData: OcrBoundingBox[], metrics: ContentM
});
}
+ const rowThreshold = metrics.contentHeight * 0.02;
+ boxes.sort((a, b) => {
+ const yDifference = a.points[0].y - b.points[0].y;
+ if (Math.abs(yDifference) < rowThreshold) {
+ return a.points[0].x - b.points[0].x;
+ }
+ return yDifference;
+ });
+
return boxes;
};