Fix aspect ratio when rendering images with kitty protocol (#6675)

Fixes https://github.com/ghostty-org/ghostty/issues/6673

<img width="914" alt="image"
src="https://github.com/user-attachments/assets/010a1304-0d46-46ec-9a82-87a8d8fbea1b"
/>
This commit is contained in:
Mitchell Hashimoto
2025-03-13 09:50:19 -07:00
committed by GitHub
3 changed files with 83 additions and 10 deletions

View File

@ -2032,13 +2032,16 @@ fn prepKittyPlacement(
break :offset_y @intCast(offset_pixels);
} else 0;
// Get the grid size that respects aspect ratio
const grid_size = p.gridSize(image.*, t);
// If we specify `rows` then our offset above is in viewport space
// and not in the coordinate space of the source image. Without `rows`
// that's one and the same.
const source_offset_y: u32 = if (p.rows > 0) source_offset_y: {
const source_offset_y: u32 = if (grid_size.rows > 0) source_offset_y: {
// Determine the scale factor to apply for this row height.
const image_height: f64 = @floatFromInt(image.height);
const viewport_height: f64 = @floatFromInt(p.rows * self.grid_metrics.cell_height);
const viewport_height: f64 = @floatFromInt(grid_size.rows * self.grid_metrics.cell_height);
const scale: f64 = image_height / viewport_height;
// Apply the scale to the offset
@ -2071,11 +2074,11 @@ fn prepKittyPlacement(
image.height -| source_y;
// Calculate the width/height of our image.
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
const dest_height = if (p.rows > 0) rows: {
const dest_width = grid_size.cols * self.grid_metrics.cell_width;
const dest_height = if (grid_size.rows > 0) rows: {
// Clip to the viewport to handle scrolling. offset_y is already in
// viewport scale so we can subtract it directly.
break :rows (p.rows * self.grid_metrics.cell_height) - offset_y;
break :rows (grid_size.rows * self.grid_metrics.cell_height) - offset_y;
} else source_height;
// Accumulate the placement

View File

@ -1073,13 +1073,16 @@ fn prepKittyPlacement(
break :offset_y @intCast(offset_pixels);
} else 0;
// Get the grid size that respects aspect ratio
const grid_size = p.gridSize(image.*, t);
// If we specify `rows` then our offset above is in viewport space
// and not in the coordinate space of the source image. Without `rows`
// that's one and the same.
const source_offset_y: u32 = if (p.rows > 0) source_offset_y: {
const source_offset_y: u32 = if (grid_size.rows > 0) source_offset_y: {
// Determine the scale factor to apply for this row height.
const image_height: f64 = @floatFromInt(image.height);
const viewport_height: f64 = @floatFromInt(p.rows * self.grid_metrics.cell_height);
const viewport_height: f64 = @floatFromInt(grid_size.rows * self.grid_metrics.cell_height);
const scale: f64 = image_height / viewport_height;
// Apply the scale to the offset
@ -1112,11 +1115,11 @@ fn prepKittyPlacement(
image.height -| source_y;
// Calculate the width/height of our image.
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
const dest_height = if (p.rows > 0) rows: {
const dest_width = grid_size.cols * self.grid_metrics.cell_width;
const dest_height = if (grid_size.rows > 0) rows: {
// Clip to the viewport to handle scrolling. offset_y is already in
// viewport scale so we can subtract it directly.
break :rows (p.rows * self.grid_metrics.cell_height) - offset_y;
break :rows (grid_size.rows * self.grid_metrics.cell_height) - offset_y;
} else source_height;
// Accumulate the placement

View File

@ -687,6 +687,33 @@ pub const ImageStorage = struct {
// Calculate our image size in grid cells
const width_f64: f64 = @floatFromInt(width_px);
const height_f64: f64 = @floatFromInt(height_px);
// If only columns is specified, calculate rows based on aspect ratio
if (self.columns > 0 and self.rows == 0) {
const cols_f64: f64 = @floatFromInt(self.columns);
const cols_px = cols_f64 * cell_width_f64;
const aspect_ratio = height_f64 / width_f64;
const rows_px = cols_px * aspect_ratio;
const rows_cells = rows_px / cell_height_f64;
return .{
.cols = self.columns,
.rows = @intFromFloat(@ceil(rows_cells)),
};
}
// If only rows is specified, calculate columns based on aspect ratio
if (self.rows > 0 and self.columns == 0) {
const rows_f64: f64 = @floatFromInt(self.rows);
const rows_px = rows_f64 * cell_height_f64;
const aspect_ratio = width_f64 / height_f64;
const cols_px = rows_px * aspect_ratio;
const cols_cells = cols_px / cell_width_f64;
return .{
.cols = @intFromFloat(@ceil(cols_cells)),
.rows = self.rows,
};
}
const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64));
const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64));
@ -1235,3 +1262,43 @@ test "storage: delete images by range 4" {
try testing.expectEqual(@as(usize, 0), s.placements.count());
try testing.expectEqual(tracked, t.screen.pages.countTrackedPins());
}
test "storage: aspect ratio calculation when only columns or rows specified" {
const testing = std.testing;
const alloc = testing.allocator;
var t = try terminal.Terminal.init(alloc, .{ .cols = 100, .rows = 100 });
defer t.deinit(alloc);
t.width_px = 100;
t.height_px = 100;
// Case 1: Only columns specified
{
const image = Image{ .id = 1, .width = 4, .height = 2 };
var placement = ImageStorage.Placement{
.location = .{ .virtual = {} },
.columns = 6,
.rows = 0,
};
const grid_size = placement.gridSize(image, &t);
// 6 columns * (2/4) = 3 rows
try testing.expectEqual(@as(u32, 6), grid_size.cols);
try testing.expectEqual(@as(u32, 3), grid_size.rows);
}
// Case 2: Only rows specified
{
const image = Image{ .id = 2, .width = 2, .height = 4 };
var placement = ImageStorage.Placement{
.location = .{ .virtual = {} },
.columns = 0,
.rows = 6,
};
const grid_size = placement.gridSize(image, &t);
// 6 rows * (2/4) = 3 columns
try testing.expectEqual(@as(u32, 3), grid_size.cols);
try testing.expectEqual(@as(u32, 6), grid_size.rows);
}
}