1
1
mirror of https://github.com/wez/wezterm.git synced 2024-12-23 13:21:38 +03:00

imgcat: add resize and resample functionality

This is primarily to improve the chances of displaying an arbitrary
image without resorting to additional external tools, that may be
difficult or impossible to install.

refs: #3716
refs: #3264
This commit is contained in:
Wez Furlong 2023-07-17 10:55:53 -07:00
parent 69e610041b
commit e048410491
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
8 changed files with 323 additions and 26 deletions

View File

@ -1451,7 +1451,7 @@ _wezterm() {
return 0
;;
wezterm__imgcat)
opts="-h --width --height --no-preserve-aspect-ratio --position --no-move-cursor --hold --help [FILE_NAME]"
opts="-h --width --height --no-preserve-aspect-ratio --position --no-move-cursor --hold --tmux-passthru --max-pixels --no-resample --resample-format --resample-filter --resize --show-resample-timing --help [FILE_NAME]"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
@ -1469,6 +1469,26 @@ _wezterm() {
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--tmux-passthru)
COMPREPLY=($(compgen -W "disable enable detect" -- "${cur}"))
return 0
;;
--max-pixels)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--resample-format)
COMPREPLY=($(compgen -W "png jpeg input" -- "${cur}"))
return 0
;;
--resample-filter)
COMPREPLY=($(compgen -W "nearest triangle catmull-rom gaussian lanczos3" -- "${cur}"))
return 0
;;
--resize)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
@ -1553,12 +1573,16 @@ _wezterm() {
return 0
;;
wezterm__set__working__directory)
opts="-h --help [CWD] [HOST]"
opts="-h --tmux-passthru --help [CWD] [HOST]"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--tmux-passthru)
COMPREPLY=($(compgen -W "disable enable detect" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;

View File

@ -161,10 +161,18 @@ complete -c wezterm -n "__fish_seen_subcommand_from cli; and __fish_seen_subcomm
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l width -d 'Specify the display width; defaults to "auto" which automatically selects an appropriate size. You may also use an integer value `N` to specify the number of cells, or `Npx` to specify the number of pixels, or `N%` to size relative to the terminal width' -r
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l height -d 'Specify the display height; defaults to "auto" which automatically selects an appropriate size. You may also use an integer value `N` to specify the number of cells, or `Npx` to specify the number of pixels, or `N%` to size relative to the terminal height' -r
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l position -d 'Set the cursor position prior to displaying the image. The default is to use the current cursor position. Coordinates are expressed in cells with 0,0 being the top left cell position' -r
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l tmux-passthru -d 'How to manage passing the escape through to tmux' -r -f -a "{disable ,enable ,detect }"
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l max-pixels -d 'Set the maximum number of pixels per image frame. Images will be scaled down so that they do not exceed this size, unless `--no-resample` is also used. The default value matches the limit set by wezterm. Note that resampling the image here will reduce any animated images to a single frame' -r
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l resample-format -d 'Specify the image format to use to encode resampled/resized images. The default is to match the input format, but you can choose an alternative format' -r -f -a "{png ,jpeg ,input }"
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l resample-filter -d 'Specify the filtering technique used when resizing/resampling images. The default is a reasonable middle ground of speed and quality' -r -f -a "{nearest ,triangle ,catmull-rom ,gaussian ,lanczos3 }"
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l resize -d 'Pre-process the image to resize it to the specified dimensions, expressed as eg: 800x600 (width x height). The resize is independent of other parameters that control the image placement and dimensions in the terminal; this is provided as a convenience preprocessing step' -r
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l no-preserve-aspect-ratio -d 'Do not respect the aspect ratio. The default is to respect the aspect ratio'
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l no-move-cursor -d 'Do not move the cursor after displaying the image. Note that when used like this from the shell, there is a very high chance that shell prompt will overwrite the image; you may wish to also use `--hold` in that case'
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l hold -d 'Wait for enter to be pressed after displaying the image'
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -s h -l help -d 'Print help'
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l no-resample -d 'Do not resample images whose frames are larger than the max-pixels value. Note that this will typically result in the image refusing to display in wezterm'
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -l show-resample-timing -d 'When resampling or resizing, display some diagnostics around the timing/performance of that operation'
complete -c wezterm -n "__fish_seen_subcommand_from imgcat" -s h -l help -d 'Print help (see more with \'--help\')'
complete -c wezterm -n "__fish_seen_subcommand_from set-working-directory" -l tmux-passthru -d 'How to manage passing the escape through to tmux' -r -f -a "{disable ,enable ,detect }"
complete -c wezterm -n "__fish_seen_subcommand_from set-working-directory" -s h -l help -d 'Print help'
complete -c wezterm -n "__fish_seen_subcommand_from record" -s h -l help -d 'Print help'
complete -c wezterm -n "__fish_seen_subcommand_from replay" -l explain -d 'Explain what is being sent/received'

View File

@ -382,16 +382,24 @@ _arguments "${_arguments_options[@]}" \
'--width=[Specify the display width; defaults to "auto" which automatically selects an appropriate size. You may also use an integer value \`N\` to specify the number of cells, or \`Npx\` to specify the number of pixels, or \`N%\` to size relative to the terminal width]:WIDTH: ' \
'--height=[Specify the display height; defaults to "auto" which automatically selects an appropriate size. You may also use an integer value \`N\` to specify the number of cells, or \`Npx\` to specify the number of pixels, or \`N%\` to size relative to the terminal height]:HEIGHT: ' \
'--position=[Set the cursor position prior to displaying the image. The default is to use the current cursor position. Coordinates are expressed in cells with 0,0 being the top left cell position]:POSITION: ' \
'--tmux-passthru=[How to manage passing the escape through to tmux]:TMUX_PASSTHRU:(disable enable detect)' \
'--max-pixels=[Set the maximum number of pixels per image frame. Images will be scaled down so that they do not exceed this size, unless \`--no-resample\` is also used. The default value matches the limit set by wezterm. Note that resampling the image here will reduce any animated images to a single frame]:MAX_PIXELS: ' \
'--resample-format=[Specify the image format to use to encode resampled/resized images. The default is to match the input format, but you can choose an alternative format]:RESAMPLE_FORMAT:(png jpeg input)' \
'--resample-filter=[Specify the filtering technique used when resizing/resampling images. The default is a reasonable middle ground of speed and quality]:RESAMPLE_FILTER:(nearest triangle catmull-rom gaussian lanczos3)' \
'--resize=[Pre-process the image to resize it to the specified dimensions, expressed as eg\: 800x600 (width x height). The resize is independent of other parameters that control the image placement and dimensions in the terminal; this is provided as a convenience preprocessing step]:WIDTHxHEIGHT: ' \
'--no-preserve-aspect-ratio[Do not respect the aspect ratio. The default is to respect the aspect ratio]' \
'--no-move-cursor[Do not move the cursor after displaying the image. Note that when used like this from the shell, there is a very high chance that shell prompt will overwrite the image; you may wish to also use \`--hold\` in that case]' \
'--hold[Wait for enter to be pressed after displaying the image]' \
'-h[Print help]' \
'--help[Print help]' \
'--no-resample[Do not resample images whose frames are larger than the max-pixels value. Note that this will typically result in the image refusing to display in wezterm]' \
'--show-resample-timing[When resampling or resizing, display some diagnostics around the timing/performance of that operation]' \
'-h[Print help (see more with '\''--help'\'')]' \
'--help[Print help (see more with '\''--help'\'')]' \
'::file_name -- The name of the image file to be displayed. If omitted, will attempt to read it from stdin:_files' \
&& ret=0
;;
(set-working-directory)
_arguments "${_arguments_options[@]}" \
'--tmux-passthru=[How to manage passing the escape through to tmux]:TMUX_PASSTHRU:(disable enable detect)' \
'-h[Print help]' \
'--help[Print help]' \
'::cwd -- The directory to specify. If omitted, will use the current directory of the process itself:_files -/' \

View File

@ -16,11 +16,20 @@ for mode in copy_mode search_mode ; do
echo "\`\`\`" >> $fname
done
cargo run --example narrow $PWD/target/debug/wezterm --help | ./target/debug/strip-ansi-escapes > docs/examples/cmd-synopsis-wezterm--help.txt
# For whatever reason, running --help on macOS vs. Linux results in different
# opinions on leading/trailing whitespace. In order to minimize diffs and
# be more consistent, explicitly trim leading/trailing space from the
# output stream.
# <https://unix.stackexchange.com/a/552191/123914>
trim_file() {
perl -0777 -pe 's/^\n+|\n\K\n+$//g'
}
cargo run --example narrow $PWD/target/debug/wezterm --help | ./target/debug/strip-ansi-escapes | trim_file > docs/examples/cmd-synopsis-wezterm--help.txt
for cmd in start ssh serial connect ls-fonts show-keys imgcat set-working-directory record replay ; do
fname="docs/examples/cmd-synopsis-wezterm-${cmd}--help.txt"
cargo run --example narrow $PWD/target/debug/wezterm $cmd --help | ./target/debug/strip-ansi-escapes > $fname
cargo run --example narrow $PWD/target/debug/wezterm $cmd --help | ./target/debug/strip-ansi-escapes | trim_file > $fname
done
for cmd in \
@ -42,5 +51,5 @@ for cmd in \
split-pane \
; do
fname="docs/examples/cmd-synopsis-wezterm-cli-${cmd}--help.txt"
cargo run --example narrow $PWD/target/debug/wezterm cli $cmd --help | ./target/debug/strip-ansi-escapes > $fname
cargo run --example narrow $PWD/target/debug/wezterm cli $cmd --help | ./target/debug/strip-ansi-escapes | trim_file > $fname
done

View File

@ -41,6 +41,11 @@ As features stabilize some brief notes about them will accumulate here.
in order to avoid the shell/prompt from mangling the image after it is printing.
Support for this has limitations and will not take effect when the new
`--position` argument is used. #3624
* [wezterm imgcat](cli/imgcat.md) will now resample very large images in
order to increase the chances of successfully displaying an arbitrary image.
In addition, there are now a number of options for explicitly resizing
as a preprocessing step, and controlling the filtering and format used
by the resizing, along with showing diagnostics around the resize operation. #3264
* Color schemes: [Ef-Cyprus](colorschemes/e/index.md#ef-cyprus),
[Ef-Day](colorschemes/e/index.md#ef-day),
[Ef-Deuteranopia-Dark](colorschemes/e/index.md#ef-deuteranopia-dark),

View File

@ -3,8 +3,9 @@ Output an image to the terminal
Usage: wezterm imgcat [OPTIONS] [FILE_NAME]
Arguments:
[FILE_NAME] The name of the image file to be displayed. If omitted, will
attempt to read it from stdin
[FILE_NAME]
The name of the image file to be displayed. If omitted, will attempt
to read it from stdin
Options:
--width <WIDTH>
@ -12,24 +13,84 @@ Options:
selects an appropriate size. You may also use an integer value `N` to
specify the number of cells, or `Npx` to specify the number of pixels,
or `N%` to size relative to the terminal width
--height <HEIGHT>
Specify the display height; defaults to "auto" which automatically
selects an appropriate size. You may also use an integer value `N` to
specify the number of cells, or `Npx` to specify the number of pixels,
or `N%` to size relative to the terminal height
--no-preserve-aspect-ratio
Do not respect the aspect ratio. The default is to respect the aspect
ratio
--position <POSITION>
Set the cursor position prior to displaying the image. The default is
to use the current cursor position. Coordinates are expressed in cells
with 0,0 being the top left cell position
--no-move-cursor
Do not move the cursor after displaying the image. Note that when used
like this from the shell, there is a very high chance that shell
prompt will overwrite the image; you may wish to also use `--hold` in
that case
--hold
Wait for enter to be pressed after displaying the image
--tmux-passthru <TMUX_PASSTHRU>
How to manage passing the escape through to tmux
[possible values: disable, enable, detect]
--max-pixels <MAX_PIXELS>
Set the maximum number of pixels per image frame. Images will be
scaled down so that they do not exceed this size, unless
`--no-resample` is also used. The default value matches the limit set
by wezterm. Note that resampling the image here will reduce any
animated images to a single frame
[default: 25000000]
--no-resample
Do not resample images whose frames are larger than the max-pixels
value. Note that this will typically result in the image refusing to
display in wezterm
--resample-format <RESAMPLE_FORMAT>
Specify the image format to use to encode resampled/resized images.
The default is to match the input format, but you can choose an
alternative format
[default: input]
[possible values: png, jpeg, input]
--resample-filter <RESAMPLE_FILTER>
Specify the filtering technique used when resizing/resampling images.
The default is a reasonable middle ground of speed and quality.
See
<https://docs.rs/image/latest/image/imageops/enum.FilterType.html#examples>
for examples of the different techniques and their tradeoffs.
[default: catmull-rom]
[possible values: nearest, triangle, catmull-rom, gaussian, lanczos3]
--resize <WIDTHxHEIGHT>
Pre-process the image to resize it to the specified dimensions,
expressed as eg: 800x600 (width x height). The resize is independent
of other parameters that control the image placement and dimensions in
the terminal; this is provided as a convenience preprocessing step.
Resizing animated images will reduce the image to a single frame.
The `--resample-filter` and `--resample-format` options give some
control over the quality of the resizing operation and the image
format used.
--show-resample-timing
When resampling or resizing, display some diagnostics around the
timing/performance of that operation
-h, --help
Print help
Print help (see a summary with '-h')

View File

@ -1,7 +1,7 @@
Advise the terminal of the current working directory by emitting an OSC 7 escape
sequence
Usage: wezterm set-working-directory [CWD] [HOST]
Usage: wezterm set-working-directory [OPTIONS] [CWD] [HOST]
Arguments:
[CWD] The directory to specify. If omitted, will use the current directory
@ -10,4 +10,8 @@ Arguments:
system hostname will be used
Options:
-h, --help Print help
--tmux-passthru <TMUX_PASSTHRU>
How to manage passing the escape through to tmux [possible values:
disable, enable, detect]
-h, --help
Print help

View File

@ -183,6 +183,56 @@ struct ImgCatCommand {
#[arg(long, value_parser)]
tmux_passthru: Option<TmuxPassthru>,
/// Set the maximum number of pixels per image frame.
/// Images will be scaled down so that they do not exceed this size,
/// unless `--no-resample` is also used.
/// The default value matches the limit set by wezterm.
/// Note that resampling the image here will reduce any animated
/// images to a single frame.
#[arg(long, default_value = "25000000")]
max_pixels: usize,
/// Do not resample images whose frames are larger than the
/// max-pixels value.
/// Note that this will typically result in the image refusing
/// to display in wezterm.
#[arg(long)]
no_resample: bool,
/// Specify the image format to use to encode resampled/resized
/// images. The default is to match the input format, but you
/// can choose an alternative format.
#[arg(long, default_value = "input")]
resample_format: ResampleImageFormat,
/// Specify the filtering technique used when resizing/resampling
/// images. The default is a reasonable middle ground of speed
/// and quality.
///
/// See <https://docs.rs/image/latest/image/imageops/enum.FilterType.html#examples>
/// for examples of the different techniques and their tradeoffs.
#[arg(long, default_value = "catmull-rom")]
resample_filter: ResampleFilter,
/// Pre-process the image to resize it to the specified dimensions,
/// expressed as eg: 800x600 (width x height).
/// The resize is independent of other parameters that control
/// the image placement and dimensions in the terminal; this is provided
/// as a convenience preprocessing step.
///
/// Resizing animated images will reduce the image to a single frame.
///
/// The `--resample-filter` and `--resample-format` options give
/// some control over the quality of the resizing operation and
/// the image format used.
#[arg(long, name="WIDTHxHEIGHT", value_parser=ValueParser::new(width_x_height))]
resize: Option<ImageDimension>,
/// When resampling or resizing, display some diagnostics
/// around the timing/performance of that operation.
#[arg(long)]
show_resample_timing: bool,
/// The name of the image file to be displayed.
/// If omitted, will attempt to read it from stdin.
#[arg(value_parser, value_hint=ValueHint::FilePath)]
@ -206,15 +256,54 @@ fn x_comma_y(arg: &str) -> Result<ImagePosition, String> {
})?;
Ok(ImagePosition { x, y })
} else {
Err(format!("Expected name=value, but got {}", arg))
Err(format!("Expected x,y, but got {}", arg))
}
}
#[derive(Debug)]
#[derive(Clone, Copy, Debug)]
struct ImageDimension {
width: u32,
height: u32,
}
fn width_x_height(arg: &str) -> Result<ImageDimension, String> {
if let Some(eq) = arg.find('x') {
let (left, right) = arg.split_at(eq);
let width = left.parse().map_err(|err| {
format!("Expected WxH to be integer values, got {arg}. '{left}': {err:#}")
})?;
let height = right[1..].parse().map_err(|err| {
format!("Expected WxH to be integer values, got {arg}. '{right}': {err:#}")
})?;
Ok(ImageDimension { width, height })
} else {
Err(format!("Expected WxH, but got {}", arg))
}
}
#[derive(Copy, Clone, Debug, ValueEnum, Default)]
enum ResampleFilter {
Nearest,
Triangle,
#[default]
CatmullRom,
Gaussian,
Lanczos3,
}
#[derive(Copy, Clone, Debug, ValueEnum, Default)]
enum ResampleImageFormat {
Png,
Jpeg,
#[default]
Input,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ImageInfo {
pub width: u32,
pub height: u32,
pub _format: image::ImageFormat,
pub format: image::ImageFormat,
}
impl ImgCatCommand {
@ -292,16 +381,78 @@ impl ImgCatCommand {
let reader = image::io::Reader::new(std::io::Cursor::new(data)).with_guessed_format()?;
let format = reader
.format()
.ok_or_else(|| anyhow::anyhow!("unknown format!?"))?;
.ok_or_else(|| anyhow::anyhow!("unknown image format!?"))?;
let (width, height) = reader.into_dimensions()?;
Ok(ImageInfo {
width,
height,
_format: format,
format,
})
}
fn run(&self) -> anyhow::Result<()> {
fn resize_image(
&self,
data: &[u8],
target_width: u32,
target_height: u32,
image_info: ImageInfo,
) -> anyhow::Result<(Vec<u8>, ImageInfo)> {
let start = std::time::Instant::now();
let im = image::load_from_memory(data).with_context(|| match self.file_name.as_ref() {
Some(file_name) => format!("loading image from file {file_name:?}"),
None => format!("loading image from stdin"),
})?;
if self.show_resample_timing {
eprintln!(
"loading image took {:?} for {} stored bytes -> {image_info:?}",
start.elapsed(),
data.len()
);
}
let start = std::time::Instant::now();
use image::imageops::FilterType;
let filter = match self.resample_filter {
ResampleFilter::Nearest => FilterType::Nearest,
ResampleFilter::Triangle => FilterType::Triangle,
ResampleFilter::CatmullRom => FilterType::CatmullRom,
ResampleFilter::Gaussian => FilterType::Gaussian,
ResampleFilter::Lanczos3 => FilterType::Lanczos3,
};
let im = im.resize_to_fill(target_width, target_height, filter);
if self.show_resample_timing {
eprintln!("resizing took {:?}", start.elapsed());
}
let mut data = vec![];
let start = std::time::Instant::now();
let output_format = match self.resample_format {
ResampleImageFormat::Png => image::ImageFormat::Png,
ResampleImageFormat::Jpeg => image::ImageFormat::Jpeg,
ResampleImageFormat::Input => image_info.format,
};
im.write_to(&mut std::io::Cursor::new(&mut data), output_format)
.with_context(|| format!("encoding resampled image as {output_format:?}"))?;
let new_info = ImageInfo {
width: target_width,
height: target_height,
format: output_format,
};
if self.show_resample_timing {
eprintln!(
"encoding took {:?} to produce {} stored bytes -> {new_info:?}",
start.elapsed(),
data.len()
);
}
Ok((data, new_info))
}
fn get_image_data(&self) -> anyhow::Result<(Vec<u8>, ImageInfo)> {
let mut data = Vec::new();
if let Some(file_name) = self.file_name.as_ref() {
let mut f = std::fs::File::open(file_name)
@ -312,6 +463,34 @@ impl ImgCatCommand {
stdin.read_to_end(&mut data)?;
}
let image_info = Self::image_dimensions(&data)?;
let (data, image_info) = if let Some(dimension) = self.resize {
self.resize_image(&data, dimension.width, dimension.height, image_info)?
} else {
(data, image_info)
};
let total_pixels = image_info.width.saturating_mul(image_info.height) as usize;
if !self.no_resample && total_pixels > self.max_pixels {
let max_area = self.max_pixels as f32;
let area = total_pixels as f32;
let scale = area / max_area;
let target_width = (image_info.width as f32 / scale).floor() as u32;
let target_height = (image_info.height as f32 / scale).floor() as u32;
self.resize_image(&data, target_width, target_height, image_info)
} else {
Ok((data, image_info))
}
}
fn run(&self) -> anyhow::Result<()> {
let (data, image_info) = self.get_image_data()?;
let caps = Capabilities::new_from_env()?;
let mut term = termwiz::terminal::new_terminal(caps)?;
term.set_raw_mode()?;
@ -357,13 +536,12 @@ impl ImgCatCommand {
let (begin, end) = self.tmux_passthru.unwrap_or_default().get();
let image_dims = Self::image_dimensions(&data)
.map(|info| self.compute_image_cell_dimensions(info, term_size));
let image_dims = self.compute_image_cell_dimensions(image_info, term_size);
if let (Ok((_cursor_x, cursor_y)), true) = (&image_dims, needs_force_cursor_move) {
if let ((_cursor_x, cursor_y), true) = (image_dims, needs_force_cursor_move) {
// Before we emit the image, we need to emit some new lines so that
// if the image would scroll the display, things end up in the right place
let new_lines = "\n".repeat(*cursor_y);
let new_lines = "\n".repeat(cursor_y);
print!("{new_lines}");
// and move back up again.
@ -371,7 +549,7 @@ impl ImgCatCommand {
// column as a result of doing this.
term.render(&[Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(-1 * (*cursor_y as isize)),
y: Position::Relative(-1 * (cursor_y as isize)),
}])?;
}
@ -389,12 +567,12 @@ impl ImgCatCommand {
)));
println!("{begin}{osc}{end}");
if let (Ok((_cursor_x, cursor_y)), true) = (&image_dims, needs_force_cursor_move) {
if let ((_cursor_x, cursor_y), true) = (image_dims, needs_force_cursor_move) {
// tell the terminal that doesn't fully understand the image sequence
// to move the cursor to where it should end up
term.render(&[Change::CursorPosition {
x: Position::Absolute(0),
y: Position::Relative(*cursor_y as isize),
y: Position::Relative(cursor_y as isize),
}])?;
} else if self.position.is_some() {
print!("{restore_cursor}");