Allow controlling size of rendered sixel images

This commit is contained in:
Matias Fontanini 2024-01-24 11:08:41 -08:00
parent f2695a93a4
commit 28d072105b
13 changed files with 144 additions and 70 deletions

1
Cargo.lock generated
View File

@ -920,6 +920,7 @@ dependencies = [
"serde_json",
"serde_with",
"serde_yaml",
"sixel-rs",
"strum",
"syntect",
"tempfile",

View File

@ -16,6 +16,7 @@ crossterm = { version = "0.27", features = ["serde"] }
hex = "0.4"
flate2 = "1.0"
image = "0.24"
sixel-rs = { version = "0.3.3", optional = true }
merge-struct = "0.1.0"
itertools = "0.12"
once_cell = "1.19"
@ -42,7 +43,8 @@ rstest = { version = "0.18", default-features = false }
[features]
default = []
sixel = ["viuer/sixel"]
# TODO viuer
sixel = ["viuer/sixel", "sixel-rs"]
[profile.dev]
opt-level = 0

View File

@ -41,12 +41,23 @@ pub enum ConfigLoadError {
Invalid(#[from] serde_yaml::Error),
}
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DefaultsConfig {
pub theme: Option<String>,
pub terminal_font_size: Option<u8>,
#[serde(default = "default_font_size")]
pub terminal_font_size: u8,
}
impl Default for DefaultsConfig {
fn default() -> Self {
Self { theme: Default::default(), terminal_font_size: default_font_size() }
}
}
fn default_font_size() -> u8 {
16
}
#[derive(Clone, Debug, Default, Deserialize)]

View File

@ -142,7 +142,7 @@ impl<'a> Exporter<'a> {
ImageSource::Generated => {
let mut buffer = Vec::new();
let dimensions = image.original.dimensions();
let ImageResource::Viuer(resource) = image.original.resource.as_ref() else {
let ImageResource::Ascii(resource) = image.original.resource.as_ref() else {
panic!("not in viuer mode")
};
PngEncoder::new(&mut buffer).write_image(
@ -268,7 +268,7 @@ impl ImageReplacer {
}
self.images.push(ReplacedImage { original: image, color });
Image::new(ImageResource::Viuer(replacement.into()), ImageSource::Generated)
Image::new(ImageResource::Ascii(replacement.into()), ImageSource::Generated)
}
fn allocate_color(&mut self) -> u32 {

View File

@ -189,10 +189,6 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
display_acknowledgements();
return Ok(());
}
if !cli.generate_pdf_metadata {
// Pre-load this so we don't flicker on the first displayed image when using viuer.
GraphicsMode::detect_graphics_protocol();
}
let path = cli.path.take().unwrap_or_else(|| {
Cli::command().error(ErrorKind::MissingRequiredArgument, "no path specified").exit();
@ -200,7 +196,7 @@ fn run(mut cli: Cli) -> Result<(), Box<dyn std::error::Error>> {
let resources_path = path.parent().unwrap_or(Path::new("/"));
let mut options = make_builder_options(&config, &mode, force_default_theme);
let graphics_mode = select_graphics_mode(&cli);
let printer = Rc::new(ImagePrinter::new(graphics_mode.clone())?);
let printer = Rc::new(ImagePrinter::new(graphics_mode.clone(), config.defaults.terminal_font_size)?);
let registry = ImageRegistry(printer.clone());
let resources = Resources::new(resources_path, registry.clone());
let typst = TypstRender::new(config.typst.ppi, registry);

View File

@ -2,22 +2,22 @@ use super::printer::{PrintImage, PrintImageError, PrintOptions, RegisterImageErr
use image::{DynamicImage, GenericImageView};
use std::{fs, ops::Deref};
pub(crate) struct ViuerResource(DynamicImage);
pub(crate) struct AsciiResource(DynamicImage);
impl ResourceProperties for ViuerResource {
impl ResourceProperties for AsciiResource {
fn dimensions(&self) -> (u32, u32) {
self.0.dimensions()
}
}
impl From<DynamicImage> for ViuerResource {
impl From<DynamicImage> for AsciiResource {
fn from(image: DynamicImage) -> Self {
let image = image.into_rgba8();
Self(image.into())
}
}
impl Deref for ViuerResource {
impl Deref for AsciiResource {
type Target = DynamicImage;
fn deref(&self) -> &Self::Target {
@ -25,38 +25,20 @@ impl Deref for ViuerResource {
}
}
#[cfg(feature = "sixel")]
#[derive(Default)]
pub(crate) enum SixelSupport {
Enabled,
#[default]
Disabled,
}
pub struct AsciiPrinter;
#[derive(Default)]
pub struct ViuerPrinter {
#[cfg(feature = "sixel")]
sixel: SixelSupport,
}
impl ViuerPrinter {
#[cfg(feature = "sixel")]
pub(crate) fn new(sixel: SixelSupport) -> Self {
Self { sixel }
}
}
impl PrintImage for ViuerPrinter {
type Resource = ViuerResource;
impl PrintImage for AsciiPrinter {
type Resource = AsciiResource;
fn register_image(&self, image: image::DynamicImage) -> Result<Self::Resource, RegisterImageError> {
Ok(ViuerResource(image))
Ok(AsciiResource(image))
}
fn register_resource<P: AsRef<std::path::Path>>(&self, path: P) -> Result<Self::Resource, RegisterImageError> {
let contents = fs::read(path)?;
let image = image::load_from_memory(&contents)?;
Ok(ViuerResource(image))
Ok(AsciiResource(image))
}
fn print<W>(&self, image: &Self::Resource, options: &PrintOptions, _writer: &mut W) -> Result<(), PrintImageError>
@ -68,10 +50,10 @@ impl PrintImage for ViuerPrinter {
height: Some(options.rows as u32),
use_kitty: false,
use_iterm: false,
#[cfg(feature = "sixel")]
use_sixel: false,
x: options.cursor_position.column,
y: options.cursor_position.row as i16,
#[cfg(feature = "sixel")]
use_sixel: matches!(self.sixel, SixelSupport::Enabled),
..Default::default()
};
viuer::print(&image.0, &config)?;

View File

@ -11,12 +11,3 @@ pub enum GraphicsMode {
#[cfg(feature = "sixel")]
Sixel,
}
impl GraphicsMode {
pub fn detect_graphics_protocol() {
viuer::is_iterm_supported();
viuer::get_kitty_support();
#[cfg(feature = "sixel")]
viuer::is_sixel_supported();
}
}

View File

@ -1,3 +1,4 @@
mod ascii;
pub(crate) mod emulator;
pub(crate) mod graphics;
pub(crate) mod image;
@ -6,4 +7,5 @@ pub(crate) mod kitty;
pub(crate) mod printer;
pub(crate) mod register;
pub(crate) mod scale;
mod viuer;
#[cfg(feature = "sixel")]
pub(crate) mod sixel;

View File

@ -1,11 +1,10 @@
use crate::render::properties::CursorPosition;
use super::{
ascii::{AsciiPrinter, AsciiResource},
graphics::GraphicsMode,
iterm::{ItermPrinter, ItermResource},
kitty::{KittyMode, KittyPrinter, KittyResource},
viuer::{ViuerPrinter, ViuerResource},
};
use crate::render::properties::CursorPosition;
use image::{DynamicImage, ImageError};
use std::{borrow::Cow, io, path::Path};
@ -38,7 +37,9 @@ pub(crate) struct PrintOptions {
pub(crate) enum ImageResource {
Kitty(KittyResource),
Iterm(ItermResource),
Viuer(ViuerResource),
Ascii(AsciiResource),
#[cfg(feature = "sixel")]
Sixel(super::sixel::SixelResource),
}
impl ResourceProperties for ImageResource {
@ -46,7 +47,9 @@ impl ResourceProperties for ImageResource {
match self {
Self::Kitty(resource) => resource.dimensions(),
Self::Iterm(resource) => resource.dimensions(),
Self::Viuer(resource) => resource.dimensions(),
Self::Ascii(resource) => resource.dimensions(),
#[cfg(feature = "sixel")]
Self::Sixel(resource) => resource.dimensions(),
}
}
}
@ -54,7 +57,9 @@ impl ResourceProperties for ImageResource {
pub enum ImagePrinter {
Kitty(KittyPrinter),
Iterm(ItermPrinter),
Viuer(ViuerPrinter),
Ascii(AsciiPrinter),
#[cfg(feature = "sixel")]
Sixel(super::sixel::SixelPrinter),
}
impl Default for ImagePrinter {
@ -64,13 +69,13 @@ impl Default for ImagePrinter {
}
impl ImagePrinter {
pub fn new(mode: GraphicsMode) -> io::Result<Self> {
pub fn new(mode: GraphicsMode, #[allow(unused_variables)] font_size: u8) -> Result<Self, CreatePrinterError> {
let printer = match mode {
GraphicsMode::Kitty { mode, inside_tmux } => Self::new_kitty(mode, inside_tmux)?,
GraphicsMode::Iterm2 => Self::new_iterm(),
GraphicsMode::AsciiBlocks => Self::new_ascii(),
#[cfg(feature = "sixel")]
GraphicsMode::Sixel => Self::new_sixel(),
GraphicsMode::Sixel => Self::new_sixel(font_size)?,
};
Ok(printer)
}
@ -84,12 +89,12 @@ impl ImagePrinter {
}
fn new_ascii() -> Self {
Self::Viuer(ViuerPrinter::default())
Self::Ascii(AsciiPrinter)
}
#[cfg(feature = "sixel")]
fn new_sixel() -> Self {
Self::Viuer(ViuerPrinter::new(super::viuer::SixelSupport::Enabled))
fn new_sixel(font_size: u8) -> Result<Self, CreatePrinterError> {
Ok(Self::Sixel(super::sixel::SixelPrinter::new(font_size)?))
}
}
@ -100,7 +105,9 @@ impl PrintImage for ImagePrinter {
let resource = match self {
Self::Kitty(printer) => ImageResource::Kitty(printer.register_image(image)?),
Self::Iterm(printer) => ImageResource::Iterm(printer.register_image(image)?),
Self::Viuer(printer) => ImageResource::Viuer(printer.register_image(image)?),
Self::Ascii(printer) => ImageResource::Ascii(printer.register_image(image)?),
#[cfg(feature = "sixel")]
Self::Sixel(printer) => ImageResource::Sixel(printer.register_image(image)?),
};
Ok(resource)
}
@ -109,7 +116,9 @@ impl PrintImage for ImagePrinter {
let resource = match self {
Self::Kitty(printer) => ImageResource::Kitty(printer.register_resource(path)?),
Self::Iterm(printer) => ImageResource::Iterm(printer.register_resource(path)?),
Self::Viuer(printer) => ImageResource::Viuer(printer.register_resource(path)?),
Self::Ascii(printer) => ImageResource::Ascii(printer.register_resource(path)?),
#[cfg(feature = "sixel")]
Self::Sixel(printer) => ImageResource::Sixel(printer.register_resource(path)?),
};
Ok(resource)
}
@ -121,12 +130,23 @@ impl PrintImage for ImagePrinter {
match (self, image) {
(Self::Kitty(printer), ImageResource::Kitty(image)) => printer.print(image, options, writer),
(Self::Iterm(printer), ImageResource::Iterm(image)) => printer.print(image, options, writer),
(Self::Viuer(printer), ImageResource::Viuer(image)) => printer.print(image, options, writer),
(Self::Ascii(printer), ImageResource::Ascii(image)) => printer.print(image, options, writer),
#[cfg(feature = "sixel")]
(Self::Sixel(printer), ImageResource::Sixel(image)) => printer.print(image, options, writer),
_ => Err(PrintImageError::Unsupported),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CreatePrinterError {
#[error("io: {0}")]
Io(#[from] io::Error),
#[error("unexpected: {0}")]
Other(String),
}
#[derive(Debug, thiserror::Error)]
pub enum PrintImageError {
#[error(transparent)]

71
src/media/sixel.rs Normal file
View File

@ -0,0 +1,71 @@
use super::printer::{
CreatePrinterError, PrintImage, PrintImageError, PrintOptions, RegisterImageError, ResourceProperties,
};
use image::{imageops::FilterType, DynamicImage, GenericImageView};
use sixel_rs::{
encoder::{Encoder, QuickFrameBuilder},
optflags::EncodePolicy,
sys::PixelFormat,
};
use std::{fs, io};
pub(crate) struct SixelResource(DynamicImage);
impl ResourceProperties for SixelResource {
fn dimensions(&self) -> (u32, u32) {
self.0.dimensions()
}
}
pub struct SixelPrinter {
encoder: Encoder,
font_size: u32,
}
impl SixelPrinter {
pub(crate) fn new(font_size: u8) -> Result<Self, CreatePrinterError> {
let encoder =
Encoder::new().map_err(|e| CreatePrinterError::Other(format!("creating sixel encoder: {e:?}")))?;
encoder
.set_encode_policy(EncodePolicy::Fast)
.map_err(|e| CreatePrinterError::Other(format!("setting encoder policy: {e:?}")))?;
Ok(Self { encoder, font_size: font_size.into() })
}
}
impl PrintImage for SixelPrinter {
type Resource = SixelResource;
fn register_image(&self, image: image::DynamicImage) -> Result<Self::Resource, RegisterImageError> {
Ok(SixelResource(image))
}
fn register_resource<P: AsRef<std::path::Path>>(&self, path: P) -> Result<Self::Resource, RegisterImageError> {
let contents = fs::read(path)?;
let image = image::load_from_memory(&contents)?;
Ok(SixelResource(image))
}
fn print<W>(&self, image: &Self::Resource, options: &PrintOptions, writer: &mut W) -> Result<(), PrintImageError>
where
W: io::Write,
{
// We're already positioned in the right place but we may not have flushed that yet.
writer.flush()?;
// This check was taken from viuer: it seems to be a bug in xterm
let width = (self.font_size * options.columns as u32).min(1000);
let height = self.font_size * 2 * options.rows as u32;
let image = image.0.resize_exact(width, height, FilterType::Triangle);
let bytes = image.into_rgba8().into_raw();
let frame = QuickFrameBuilder::new()
.width(width as usize)
.height(height as usize)
.format(PixelFormat::RGBA8888)
.pixels(bytes);
self.encoder.encode_bytes(frame).map_err(|e| PrintImageError::other(format!("encoding sixel image: {e:?}")))?;
Ok(())
}
}

View File

@ -27,7 +27,7 @@ use std::{
pub struct PresenterOptions {
pub mode: PresentMode,
pub builder_options: PresentationBuilderOptions,
pub font_size_fallback: Option<u8>,
pub font_size_fallback: u8,
pub bindings: KeyBindingsConfig,
}

View File

@ -15,7 +15,7 @@ pub(crate) type RenderResult = Result<(), RenderError>;
/// Allows drawing elements in the terminal.
pub(crate) struct TerminalDrawer<W: io::Write> {
terminal: Terminal<W>,
font_size_fallback: Option<u8>,
font_size_fallback: u8,
}
impl<W> TerminalDrawer<W>
@ -23,7 +23,7 @@ where
W: io::Write,
{
/// Construct a drawer over a [std::io::Write].
pub(crate) fn new(handle: W, image_printer: Rc<ImagePrinter>, font_size_fallback: Option<u8>) -> io::Result<Self> {
pub(crate) fn new(handle: W, image_printer: Rc<ImagePrinter>, font_size_fallback: u8) -> io::Result<Self> {
let terminal = Terminal::new(handle, image_printer)?;
Ok(Self { terminal, font_size_fallback })
}

View File

@ -1,8 +1,6 @@
use crossterm::terminal;
use std::io::{self, ErrorKind};
const DEFAULT_FONT_SIZE_FALLBACK: u8 = 16;
/// The size of the terminal window.
///
/// This is the same as [crossterm::terminal::window_size] except with some added functionality,
@ -17,7 +15,7 @@ pub(crate) struct WindowSize {
impl WindowSize {
/// Get the current window size.
pub(crate) fn current(font_size_fallback: Option<u8>) -> io::Result<Self> {
pub(crate) fn current(font_size_fallback: u8) -> io::Result<Self> {
let mut size: Self = match terminal::window_size() {
Ok(size) => size.into(),
Err(e) if e.kind() == ErrorKind::Unsupported => {
@ -27,7 +25,7 @@ impl WindowSize {
}
Err(e) => return Err(e),
};
let font_size_fallback = font_size_fallback.unwrap_or(DEFAULT_FONT_SIZE_FALLBACK) as u16;
let font_size_fallback = font_size_fallback as u16;
if size.width == 0 {
size.width = size.columns * font_size_fallback;
}