fix(tui): cap portrait image render height
This commit is contained in:
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed inline image rendering to cap portrait images by height instead of always scaling them to the configured maximum width.
|
||||||
|
|
||||||
## [0.74.0] - 2026-05-07
|
## [0.74.0] - 2026-05-07
|
||||||
|
|
||||||
## [0.73.1] - 2026-05-07
|
## [0.73.1] - 2026-05-07
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
allocateImageId,
|
allocateImageId,
|
||||||
getCapabilities,
|
getCapabilities,
|
||||||
|
getCellDimensions,
|
||||||
getImageDimensions,
|
getImageDimensions,
|
||||||
type ImageDimensions,
|
type ImageDimensions,
|
||||||
imageFallback,
|
imageFallback,
|
||||||
@@ -61,7 +62,10 @@ export class Image implements Component {
|
|||||||
return this.cachedLines;
|
return this.cachedLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60);
|
const maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60));
|
||||||
|
const cellDimensions = getCellDimensions();
|
||||||
|
const defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));
|
||||||
|
const maxHeight = this.options.maxHeightCells ?? defaultMaxHeight;
|
||||||
|
|
||||||
const caps = getCapabilities();
|
const caps = getCapabilities();
|
||||||
let lines: string[];
|
let lines: string[];
|
||||||
@@ -72,6 +76,7 @@ export class Image implements Component {
|
|||||||
}
|
}
|
||||||
const result = renderImage(this.base64Data, this.dimensions, {
|
const result = renderImage(this.base64Data, this.dimensions, {
|
||||||
maxWidthCells: maxWidth,
|
maxWidthCells: maxWidth,
|
||||||
|
maxHeightCells: maxHeight,
|
||||||
imageId: this.imageId,
|
imageId: this.imageId,
|
||||||
moveCursor: false,
|
moveCursor: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -211,16 +211,43 @@ export function encodeITerm2(
|
|||||||
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ImageCellSize {
|
||||||
|
columns: number;
|
||||||
|
rows: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateImageCellSize(
|
||||||
|
imageDimensions: ImageDimensions,
|
||||||
|
maxWidthCells: number,
|
||||||
|
maxHeightCells?: number,
|
||||||
|
cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },
|
||||||
|
): ImageCellSize {
|
||||||
|
const maxWidth = Math.max(1, Math.floor(maxWidthCells));
|
||||||
|
const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
|
||||||
|
const imageWidth = Math.max(1, imageDimensions.widthPx);
|
||||||
|
const imageHeight = Math.max(1, imageDimensions.heightPx);
|
||||||
|
|
||||||
|
const widthScale = (maxWidth * cellDimensions.widthPx) / imageWidth;
|
||||||
|
const heightScale = maxHeight === undefined ? widthScale : (maxHeight * cellDimensions.heightPx) / imageHeight;
|
||||||
|
const scale = Math.min(widthScale, heightScale);
|
||||||
|
|
||||||
|
const scaledWidthPx = imageWidth * scale;
|
||||||
|
const scaledHeightPx = imageHeight * scale;
|
||||||
|
const columns = Math.ceil(scaledWidthPx / cellDimensions.widthPx);
|
||||||
|
const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: Math.max(1, Math.min(maxWidth, columns)),
|
||||||
|
rows: Math.max(1, maxHeight === undefined ? rows : Math.min(maxHeight, rows)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function calculateImageRows(
|
export function calculateImageRows(
|
||||||
imageDimensions: ImageDimensions,
|
imageDimensions: ImageDimensions,
|
||||||
targetWidthCells: number,
|
targetWidthCells: number,
|
||||||
cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },
|
cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },
|
||||||
): number {
|
): number {
|
||||||
const targetWidthPx = targetWidthCells * cellDimensions.widthPx;
|
return calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).rows;
|
||||||
const scale = targetWidthPx / imageDimensions.widthPx;
|
|
||||||
const scaledHeightPx = imageDimensions.heightPx * scale;
|
|
||||||
const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
|
|
||||||
return Math.max(1, rows);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPngDimensions(base64Data: string): ImageDimensions | null {
|
export function getPngDimensions(base64Data: string): ImageDimensions | null {
|
||||||
@@ -376,25 +403,25 @@ export function renderImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const maxWidth = options.maxWidthCells ?? 80;
|
const maxWidth = options.maxWidthCells ?? 80;
|
||||||
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
|
const size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions());
|
||||||
|
|
||||||
if (caps.images === "kitty") {
|
if (caps.images === "kitty") {
|
||||||
const sequence = encodeKitty(base64Data, {
|
const sequence = encodeKitty(base64Data, {
|
||||||
columns: maxWidth,
|
columns: size.columns,
|
||||||
rows,
|
rows: size.rows,
|
||||||
imageId: options.imageId,
|
imageId: options.imageId,
|
||||||
moveCursor: options.moveCursor,
|
moveCursor: options.moveCursor,
|
||||||
});
|
});
|
||||||
return { sequence, rows, imageId: options.imageId };
|
return { sequence, rows: size.rows, imageId: options.imageId };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (caps.images === "iterm2") {
|
if (caps.images === "iterm2") {
|
||||||
const sequence = encodeITerm2(base64Data, {
|
const sequence = encodeITerm2(base64Data, {
|
||||||
width: maxWidth,
|
width: size.columns,
|
||||||
height: "auto",
|
height: "auto",
|
||||||
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
||||||
});
|
});
|
||||||
return { sequence, rows };
|
return { sequence, rows: size.rows };
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -312,6 +312,40 @@ describe("Kitty image cursor movement", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("honors maxHeightCells by reducing rendered width", () => {
|
||||||
|
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
||||||
|
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
||||||
|
try {
|
||||||
|
const result = renderImage("AAAA", { widthPx: 10, heightPx: 100 }, { maxWidthCells: 10, maxHeightCells: 5 });
|
||||||
|
assert.ok(result);
|
||||||
|
assert.strictEqual(result.rows, 5);
|
||||||
|
assert.ok(result.sequence.includes(",c=1,r=5"));
|
||||||
|
} finally {
|
||||||
|
resetCapabilitiesCache();
|
||||||
|
setCellDimensions({ widthPx: 9, heightPx: 18 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("caps Image component height to a square pixel box by default", () => {
|
||||||
|
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
||||||
|
setCellDimensions({ widthPx: 10, heightPx: 20 });
|
||||||
|
try {
|
||||||
|
const image = new Image(
|
||||||
|
"AAAA",
|
||||||
|
"image/png",
|
||||||
|
{ fallbackColor: (value) => value },
|
||||||
|
{ maxWidthCells: 10 },
|
||||||
|
{ widthPx: 10, heightPx: 100 },
|
||||||
|
);
|
||||||
|
const lines = image.render(12);
|
||||||
|
assert.strictEqual(lines.length, 5);
|
||||||
|
assert.ok(lines[0].includes(",c=1,r=5"));
|
||||||
|
} finally {
|
||||||
|
resetCapabilitiesCache();
|
||||||
|
setCellDimensions({ widthPx: 9, heightPx: 18 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("places image sequence on first line with empty padding rows", () => {
|
it("places image sequence on first line with empty padding rows", () => {
|
||||||
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
||||||
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
||||||
|
|||||||
Reference in New Issue
Block a user