-
-
Notifications
You must be signed in to change notification settings - Fork 800
/
image.rs
319 lines (290 loc) · 11.2 KB
/
image.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
use crate::{Position, StableRowIndex, TerminalState};
use anyhow::Context;
use humansize::{SizeFormatter, DECIMAL};
use num_traits::{One, Zero};
use ordered_float::NotNan;
use std::sync::Arc;
use termwiz::cell::Cell;
use termwiz::image::{ImageCell, ImageDataType};
use termwiz::surface::change::ImageData;
use termwiz::surface::TextureCoordinate;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlacementInfo {
pub first_row: StableRowIndex,
pub rows: usize,
pub cols: usize,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ImageAttachParams {
/// Dimensions of the underlying ImageData, in pixels
pub image_width: u32,
pub image_height: u32,
/// Dimensions of the area of the image to be displayed, in pixels
pub source_width: Option<u32>,
pub source_height: Option<u32>,
/// Origin of the source data region, top left corner in pixels
pub source_origin_x: u32,
pub source_origin_y: u32,
/// When rendering in the cell, use this offset from the top left
/// of the cell. This is only used in the Kitty image protocol.
/// This should be smaller than the size of the cell. Larger values will
/// be truncated.
pub cell_padding_left: u16,
pub cell_padding_top: u16,
/// Plane on which to display the image
pub z_index: i32,
/// Desired number of cells to span.
/// If None, then compute based on source_width and source_height
pub columns: Option<usize>,
pub rows: Option<usize>,
pub image_id: Option<u32>,
pub placement_id: Option<u32>,
pub style: ImageAttachStyle,
pub do_not_move_cursor: bool,
pub data: Arc<ImageData>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageAttachStyle {
Sixel,
Iterm,
Kitty,
}
impl TerminalState {
pub(crate) fn assign_image_to_cells(
&mut self,
params: ImageAttachParams,
) -> anyhow::Result<PlacementInfo> {
let seqno = self.seqno;
let physical_cols = self.screen().physical_cols;
let physical_rows = self.screen().physical_rows;
let cell_pixel_width = self.pixel_width / physical_cols;
let cell_pixel_height = self.pixel_height / physical_rows;
let cell_padding_left = params
.cell_padding_left
.min(cell_pixel_width.saturating_sub(1) as u16);
let cell_padding_top = params
.cell_padding_top
.min(cell_pixel_height.saturating_sub(1) as u16);
//NOTE: review conflicting origin vs drawing going over image
let image_max_width = params.image_width.saturating_sub(params.source_origin_x);
let image_max_height = params.image_height.saturating_sub(params.source_origin_y);
let draw_width = params
.source_width
.unwrap_or(image_max_width)
.min(image_max_width);
let draw_height = params
.source_height
.unwrap_or(image_max_height)
.min(image_max_height);
let (fullcells_width, remainder_width_cell, x_delta_divisor) = params
.columns
.map(|cols| {
(
cols,
0,
(cols * cell_pixel_width) as u32 * params.image_width / draw_width,
)
})
.unwrap_or_else(|| {
(
draw_width as usize / cell_pixel_width,
draw_width as usize % cell_pixel_width,
params.image_width,
)
});
let (fullcells_height, remainder_height_cell, y_delta_divisor) = params
.rows
.map(|rows| {
(
rows,
0,
(rows * cell_pixel_height) as u32 * params.image_height / draw_height,
)
})
.unwrap_or_else(|| {
(
draw_height as usize / cell_pixel_height,
draw_height as usize % cell_pixel_height,
params.image_height,
)
});
let target_pixel_width = fullcells_width * cell_pixel_width + remainder_width_cell;
let target_pixel_height = fullcells_height * cell_pixel_height + remainder_height_cell;
let first_row = self.screen().visible_row_to_stable_row(self.cursor.y);
let mut ypos = NotNan::new(params.source_origin_y as f32 / params.image_height as f32)
.with_context(|| format!("computing ypos {params:#?}"))?;
let start_xpos = NotNan::new(params.source_origin_x as f32 / params.image_width as f32)
.context("computing xpos")?;
let cursor_x = self.cursor.x;
let width_in_cells = fullcells_width + one_or_zero::<usize>(remainder_width_cell > 0);
let height_in_cells = fullcells_height + one_or_zero::<usize>(remainder_height_cell > 0);
let height_in_cells = if params.do_not_move_cursor {
height_in_cells.min(self.screen().physical_rows - self.cursor.y as usize)
} else {
height_in_cells
};
log::debug!(
"image is {}x{} cells (cell is {}x{}), target pixel dims {}x{}, {:?}, (term is {}x{}@{}x{})",
width_in_cells,
height_in_cells,
cell_pixel_width,
cell_pixel_height,
target_pixel_width,
target_pixel_height,
params,
physical_cols,
physical_rows,
self.pixel_width,
self.pixel_height
);
let mut remain_y = target_pixel_height;
for y in 0..height_in_cells {
let padding_bottom = cell_pixel_height.saturating_sub(remain_y) as u16;
let y_delta = (remain_y.min(cell_pixel_height) as f32) / y_delta_divisor as f32;
remain_y = remain_y.saturating_sub(cell_pixel_height);
let mut xpos = start_xpos;
let cursor_y = if params.do_not_move_cursor {
self.cursor.y + y as i64
} else {
self.cursor.y
};
log::debug!(
"setting cells for y={} x=[{}..{}]",
cursor_y,
cursor_x,
cursor_x + fullcells_width
);
let mut remain_x = target_pixel_width;
for x in 0..width_in_cells {
let padding_right = cell_pixel_width.saturating_sub(remain_x) as u16;
let x_delta = (remain_x.min(cell_pixel_width) as f32) / x_delta_divisor as f32;
remain_x = remain_x.saturating_sub(cell_pixel_width);
log::debug!(
"x_delta {} ({} px), y_delta {} ({} px), padding_right={}, padding_bottom={}",
x_delta,
x_delta * x_delta_divisor as f32,
y_delta,
y_delta * y_delta_divisor as f32,
padding_right,
padding_bottom
);
let mut cell = self
.screen_mut()
.get_cell(cursor_x + x, cursor_y)
.cloned()
.unwrap_or_else(Cell::blank);
let img = Box::new(ImageCell::with_z_index(
TextureCoordinate::new(xpos, ypos),
TextureCoordinate::new(xpos + x_delta, ypos + y_delta),
params.data.clone(),
params.z_index,
cell_padding_left,
cell_padding_top,
padding_right,
padding_bottom,
params.image_id,
params.placement_id,
));
match params.style {
ImageAttachStyle::Kitty => cell.attrs_mut().attach_image(img),
ImageAttachStyle::Sixel | ImageAttachStyle::Iterm => {
cell.attrs_mut().set_image(img)
}
};
self.screen_mut()
.set_cell(cursor_x + x, cursor_y, &cell, seqno);
xpos += x_delta;
}
ypos += y_delta;
if !params.do_not_move_cursor && y < height_in_cells - 1 {
self.new_line(false);
}
}
// adjust cursor position if the drawn cells move beyond current cell
let x_padding_shift: i64 = one_or_zero(
draw_width as usize + cell_padding_left as usize > cell_pixel_width * width_in_cells,
);
let y_padding_shift: i64 = one_or_zero(
draw_height as usize + cell_padding_top as usize > cell_pixel_height * height_in_cells,
);
if !params.do_not_move_cursor {
// Sixel places the cursor under the left corner of the image,
// unless sixel_scrolls_right is enabled.
// iTerm places it after the bottom right corner.
let bottom_right = match params.style {
ImageAttachStyle::Kitty | ImageAttachStyle::Iterm => true,
ImageAttachStyle::Sixel => self.sixel_scrolls_right,
};
if bottom_right {
self.set_cursor_pos(
&Position::Relative(width_in_cells as i64 + x_padding_shift),
&Position::Relative(y_padding_shift),
);
}
}
Ok(PlacementInfo {
first_row,
rows: height_in_cells,
cols: width_in_cells,
})
}
/// cache recent images and avoid assigning a new id for repeated data!
pub(crate) fn raw_image_to_image_data(
&mut self,
data: ImageDataType,
) -> Result<Arc<ImageData>, termwiz::error::InternalError> {
let key = data.compute_hash();
if let Some(item) = self.image_cache.get(&key) {
Ok(Arc::clone(item))
} else {
let data = data.swap_out()?;
let image_data = Arc::new(ImageData::with_data(data));
self.image_cache.put(key, Arc::clone(&image_data));
Ok(image_data)
}
}
}
pub(crate) fn check_image_dimensions(width: u32, height: u32) -> anyhow::Result<()> {
const MAX_IMAGE_SIZE: u32 = 100_000_000;
let size = width.saturating_mul(height).saturating_mul(4);
if size > MAX_IMAGE_SIZE {
anyhow::bail!(
"Ignoring image data for image with dimensions {}x{} \
because required RAM {} > max allowed {}",
width,
height,
SizeFormatter::new(size, DECIMAL),
SizeFormatter::new(MAX_IMAGE_SIZE, DECIMAL),
);
}
if size == 0 {
anyhow::bail!("Ignoring image with 0x0 dimensions");
}
Ok(())
}
#[derive(Debug)]
pub(crate) struct ImageInfo {
pub width: u32,
pub height: u32,
pub format: image::ImageFormat,
}
pub(crate) fn dimensions(data: &[u8]) -> anyhow::Result<ImageInfo> {
let reader = image::ImageReader::new(std::io::Cursor::new(data)).with_guessed_format()?;
let format = reader
.format()
.ok_or_else(|| anyhow::anyhow!("unknown format!?"))?;
let (width, height) = reader.into_dimensions()?;
Ok(ImageInfo {
width,
height,
format,
})
}
/// Returns `1` if `b` is true, else `0`,
fn one_or_zero<T: Zero + One>(b: bool) -> T {
if b {
T::one()
} else {
T::zero()
}
}