1export const MARKDOWN_IMAGE_MAX_WIDTH = 480;
2export const MARKDOWN_IMAGE_MAX_HEIGHT = 480;
3
4export interface MarkdownImageDisplaySize {
5 readonly width: number;
6 readonly height: number;
7}
8
9/** SVG percentages need a viewport; use the intrinsic viewBox in that case. */
10export function resolveSvgImageSize(props: Readonly<Record<string, unknown>>) {
11 const dimension = (value: unknown) => {
12 if (typeof value === "number") return value;
13 if (typeof value !== "string" || !/^\s*\d+(?:\.\d+)?(?:px)?\s*$/.test(value)) return NaN;
14 return Number.parseFloat(value);
15 };
16 const width = dimension(props.width);
17 const height = dimension(props.height);
18 if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0)
19 return { width, height };
20 const viewBox =
21 typeof props.viewBox === "string"
22 ? props.viewBox
23 .trim()
24 .split(/[\s,]+/)
25 .map(Number)
26 : [];
27 if (
28 viewBox.length !== 4 ||
29 !viewBox.every(Number.isFinite) ||
30 viewBox[2]! <= 0 ||
31 viewBox[3]! <= 0
32 )
33 return null;
34 return { width: viewBox[2]!, height: viewBox[3]! };
35}
36
37/** Keeps small images intrinsic while fitting larger images inside the chat viewport. */
38export function resolveMarkdownImageDisplaySize(input: {
39 readonly sourceWidth: number;
40 readonly sourceHeight: number;
41 readonly availableWidth: number;
42}): MarkdownImageDisplaySize | null {
43 if (
44 !Number.isFinite(input.sourceWidth) ||
45 !Number.isFinite(input.sourceHeight) ||
46 !Number.isFinite(input.availableWidth) ||
47 input.sourceWidth <= 0 ||
48 input.sourceHeight <= 0 ||
49 input.availableWidth <= 0
50 ) {
51 return null;
52 }
53
54 const scale = Math.min(
55 1,
56 input.availableWidth / input.sourceWidth,
57 MARKDOWN_IMAGE_MAX_WIDTH / input.sourceWidth,
58 MARKDOWN_IMAGE_MAX_HEIGHT / input.sourceHeight,
59 );
60
61 return {
62 width: input.sourceWidth * scale,
63 height: input.sourceHeight * scale,
64 };
65}