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
//! Encode an image with [sixel-sys].
//!
//! [sixel-sys]: https://crates.io/crates/sixel-sys
//!
//! ⚠️ This is my first crate that uses `unsafe` and FFI. Please inspect the source code yourself, the
//! crate is very small. PRs are welcome.
//!
//! To write a sixel to a file, [sixel-rs] is safer and has more options.
//!
//! Despite being called sixel-bytes, this crates produces a `String`.
//!
//! [sixel-rs]: https://crates.io/crates/sixel-rs
//!
//! # Examples
//!
//! Encode a generated image to sixel and print it:
//! ```rust
//! let mut bytes: Vec<u8> = Vec::new();
//! for x in 0..255 {
//! for y in 0..255 {
//! bytes.append(&mut vec![x, 0, y]);
//! }
//! }
//!
//! let data = sixel_bytes::sixel_string(
//! &bytes,
//! 255,
//! 255,
//! sixel_bytes::PixelFormat::RGB888,
//! sixel_bytes::DiffusionMethod::Atkinson,
//! ).unwrap();
//! assert_eq!(&data[..3], "\u{1b}Pq");
//! ```
//!
//! Encode an image from the [image] crate to sixel and print it:
//! ```ignore
//! let image = image::io::Reader::open("./assets/Ada.png")
//! .unwrap()
//! .decode()
//! .unwrap()
//! .into_rgba8();
//! let bytes = image.as_raw();
//!
//! match sixel_bytes::sixel_string(
//! bytes,
//! image.width() as _,
//! image.height() as _,
//! sixel_bytes::PixelFormat::RGBA8888,
//! sixel_sys::DiffusionMethod::Stucki,
//! ) {
//! Err(err) => eprintln!("{err}"),
//! Ok(data) => print!("{data}"),
//! }
//! ```
//!
//! # Binaries
//!
//! `sixel <path/to/image>` uses the [image] crate to load an image with supported formats, convert
//! to RGBA8888, encode to sixel, and dump the resulting string to stdout. It must be built with
//! the `image` feature.
//!
//! `test-sixel` just generates some 255x255 image with a gradient and dumps it to stdout.
//!
//! Only certain terminals / terminal emulators have the capability to render sixel graphics.
//! See https://www.arewesixelyet.com/ for a list of programs that support sixels.
//!
//! Try running `xterm` with `-ti 340`.
//!
//! # Features
//! The `image` feature is disabled by default but needed for the `sixel` binary.
//!
//! [image]: https://crates.io/crates/image
use core::fmt;
use std::{
ffi::{c_int, c_uchar, c_void},
mem, ptr, slice,
string::FromUtf8Error,
};
pub use sixel_sys::status;
pub use sixel_sys::status::Status;
pub use sixel_sys::DiffusionMethod;
pub use sixel_sys::PixelFormat;
use sixel_sys::{
sixel_dither_destroy, sixel_dither_initialize, sixel_dither_new,
sixel_dither_set_diffusion_type, sixel_dither_set_pixelformat, sixel_encode,
sixel_output_destroy, sixel_output_new, sixel_output_set_encode_policy, Dither, EncodePolicy,
MethodForLargest, Output,
};
#[derive(Debug)]
pub enum SixelError {
Sixel(Status),
Utf8(FromUtf8Error),
}
impl fmt::Display for SixelError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SixelError::Sixel(status) => write!(f, "Sixel error code {0}", status),
SixelError::Utf8(utf8_error) => utf8_error.fmt(f),
}
}
}
impl SixelError {
/// This is not exactly [TryFrom] nor [From]: `status::OK` produces `Ok(())`, other statuses
/// `Err(SixelError)`.
///
/// ```no_run
/// # use sixel_bytes::{SixelError, Status, status};
/// # fn some_sixel_sys_function() -> Status {
/// # status::ERR
/// # }
/// SixelError::from_status(some_sixel_sys_function())?;
/// # Ok::<(), SixelError>(())
/// ```
pub fn from_status(value: c_int) -> Result<(), Self> {
match value {
status::OK => Ok(()),
code => Err(SixelError::Sixel(code)),
}
}
}
// According to sixel-sys, this is unused/ignored.
const DEPTH_ALWAYS_IGNORED: i32 = 24;
/// Encode image bytes to a [String] containing the sixel data.
///
/// The `bytes` must match the width, height, and "pixelformat".
pub fn sixel_string(
bytes: &[u8],
width: i32,
height: i32,
pixelformat: PixelFormat,
method_for_diffuse: DiffusionMethod,
) -> Result<String, SixelError> {
let mut sixel_data: Vec<i8> = Vec::new();
let sixel_data_ptr: *mut c_void = &mut sixel_data as *mut _ as *mut c_void;
let mut output: *mut Output = ptr::null_mut() as *mut _;
let output_ptr: *mut *mut Output = &mut output as *mut _;
let mut dither: *mut Dither = ptr::null_mut() as *mut _;
let dither_ptr: *mut *mut Dither = &mut dither as *mut _;
let pixels = bytes.as_ptr() as *mut c_uchar;
unsafe extern "C" fn callback(
data: *mut ::std::os::raw::c_char,
size: ::std::os::raw::c_int,
priv_: *mut ::std::os::raw::c_void,
) -> ::std::os::raw::c_int {
let sixel_data: &mut Vec<i8> = &mut *(priv_ as *mut Vec<i8>);
let data_slice: &mut [i8] =
slice::from_raw_parts_mut(if data.is_null() { return 1 } else { data }, size as usize);
sixel_data.append(&mut data_slice.to_vec());
status::OK
}
unsafe {
SixelError::from_status(sixel_output_new(
output_ptr,
Some(callback),
sixel_data_ptr,
ptr::null_mut(),
))?;
sixel_output_set_encode_policy(output, EncodePolicy::Auto);
SixelError::from_status(sixel_dither_new(dither_ptr, 256, ptr::null_mut()))?;
SixelError::from_status(sixel_dither_initialize(
dither,
pixels,
width,
height,
pixelformat,
MethodForLargest::Auto,
sixel_sys::MethodForRepColor::Auto,
sixel_sys::QualityMode::Auto,
))?;
sixel_dither_set_pixelformat(dither, pixelformat);
sixel_dither_set_diffusion_type(dither, method_for_diffuse);
SixelError::from_status(sixel_encode(
pixels,
width,
height,
DEPTH_ALWAYS_IGNORED,
dither,
output,
))?;
sixel_output_destroy(output);
sixel_dither_destroy(dither);
// TODO: should we just return something like [u8]? Is all sixel data valid utf8?
String::from_utf8(mem::transmute(sixel_data)).map_err(SixelError::Utf8)
}
}