sixel_bytes/
lib.rs

1//! Encode an image with [sixel-sys].
2//!
3//! [sixel-sys]: https://crates.io/crates/sixel-sys
4//!
5//! ⚠️ This is my first crate that uses `unsafe` and FFI. Please inspect the source code yourself, the
6//! crate is very small. PRs are welcome.
7//!
8//! To write a sixel to a file, [sixel-rs] is safer and has more options.
9//!
10//! Despite being called sixel-bytes, this crates produces a `String`.
11//!
12//! [sixel-rs]: https://crates.io/crates/sixel-rs
13//!
14//! # Examples
15//!
16//! Encode a generated image to sixel and print it:
17//! ```rust
18//! let mut bytes: Vec<u8> = Vec::new();
19//! for x in 0..255 {
20//!     for y in 0..255 {
21//!         bytes.append(&mut vec![x, 0, y]);
22//!     }
23//! }
24//!
25//! let data = sixel_bytes::sixel_string(
26//!     &bytes,
27//!     255,
28//!     255,
29//!     sixel_bytes::PixelFormat::RGB888,
30//!     sixel_bytes::DiffusionMethod::Atkinson,
31//! ).unwrap();
32//! assert_eq!(&data[..3], "\u{1b}Pq");
33//! ```
34//!
35//! Encode an image from the [image] crate to sixel and print it:
36//! ```ignore
37//! let image = image::io::Reader::open("./assets/Ada.png")
38//!     .unwrap()
39//!     .decode()
40//!     .unwrap()
41//!     .into_rgba8();
42//! let bytes = image.as_raw();
43//!
44//! match sixel_bytes::sixel_string(
45//!     bytes,
46//!     image.width() as _,
47//!     image.height() as _,
48//!     sixel_bytes::PixelFormat::RGBA8888,
49//!     sixel_sys::DiffusionMethod::Stucki,
50//! ) {
51//!     Err(err) => eprintln!("{err}"),
52//!     Ok(data) => print!("{data}"),
53//! }
54//! ```
55//!
56//! # Binaries
57//!
58//! `sixel <path/to/image>` uses the [image] crate to load an image with supported formats, convert
59//! to RGBA8888, encode to sixel, and dump the resulting string to stdout. It must be built with
60//! the `image` feature.
61//!
62//! `test-sixel` just generates some 255x255 image with a gradient and dumps it to stdout.
63//!
64//! Only certain terminals / terminal emulators have the capability to render sixel graphics.
65//! See https://www.arewesixelyet.com/ for a list of programs that support sixels.
66//!
67//! Try running `xterm` with `-ti 340`.
68//!
69//! # Features
70//! The `image` feature is disabled by default but needed for the `sixel` binary.
71//!
72//! [image]: https://crates.io/crates/image
73
74use core::fmt;
75use std::{
76    ffi::{c_int, c_uchar, c_void},
77    mem, ptr, slice,
78    string::FromUtf8Error,
79};
80
81pub use sixel_sys::status;
82pub use sixel_sys::status::Status;
83pub use sixel_sys::DiffusionMethod;
84pub use sixel_sys::PixelFormat;
85use sixel_sys::{
86    sixel_dither_destroy, sixel_dither_initialize, sixel_dither_new,
87    sixel_dither_set_diffusion_type, sixel_dither_set_pixelformat, sixel_encode,
88    sixel_output_destroy, sixel_output_new, sixel_output_set_encode_policy, Dither, EncodePolicy,
89    MethodForLargest, Output,
90};
91
92#[derive(Debug)]
93pub enum SixelError {
94    Sixel(Status),
95    Utf8(FromUtf8Error),
96}
97
98impl fmt::Display for SixelError {
99    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
100        match self {
101            SixelError::Sixel(status) => write!(f, "Sixel error code {0}", status),
102            SixelError::Utf8(utf8_error) => utf8_error.fmt(f),
103        }
104    }
105}
106
107impl SixelError {
108    /// This is not exactly [TryFrom] nor [From]: `status::OK` produces `Ok(())`, other statuses
109    /// `Err(SixelError)`.
110    ///
111    /// ```no_run
112    /// # use sixel_bytes::{SixelError, Status, status};
113    /// # fn some_sixel_sys_function() -> Status {
114    /// #     status::ERR
115    /// # }
116    /// SixelError::from_status(some_sixel_sys_function())?;
117    /// # Ok::<(), SixelError>(())
118    /// ```
119    pub fn from_status(value: c_int) -> Result<(), Self> {
120        match value {
121            status::OK => Ok(()),
122            code => Err(SixelError::Sixel(code)),
123        }
124    }
125}
126
127// According to sixel-sys, this is unused/ignored.
128const DEPTH_ALWAYS_IGNORED: i32 = 24;
129
130/// Encode image bytes to a [String] containing the sixel data.
131///
132/// The `bytes` must match the width, height, and "pixelformat".
133pub fn sixel_string(
134    bytes: &[u8],
135    width: i32,
136    height: i32,
137    pixelformat: PixelFormat,
138    method_for_diffuse: DiffusionMethod,
139) -> Result<String, SixelError> {
140    let mut sixel_data: Vec<i8> = Vec::new();
141    let sixel_data_ptr: *mut c_void = &mut sixel_data as *mut _ as *mut c_void;
142
143    let mut output: *mut Output = ptr::null_mut() as *mut _;
144    let output_ptr: *mut *mut Output = &mut output as *mut _;
145
146    let mut dither: *mut Dither = ptr::null_mut() as *mut _;
147    let dither_ptr: *mut *mut Dither = &mut dither as *mut _;
148
149    let pixels = bytes.as_ptr() as *mut c_uchar;
150
151    unsafe extern "C" fn callback(
152        data: *mut ::std::os::raw::c_char,
153        size: ::std::os::raw::c_int,
154        priv_: *mut ::std::os::raw::c_void,
155    ) -> ::std::os::raw::c_int {
156        let sixel_data: &mut Vec<i8> = &mut *(priv_ as *mut Vec<i8>);
157
158        let data_slice: &mut [i8] =
159            slice::from_raw_parts_mut(if data.is_null() { return 1 } else { data }, size as usize);
160        sixel_data.append(&mut data_slice.to_vec());
161        status::OK
162    }
163
164    unsafe {
165        SixelError::from_status(sixel_output_new(
166            output_ptr,
167            Some(callback),
168            sixel_data_ptr,
169            ptr::null_mut(),
170        ))?;
171
172        sixel_output_set_encode_policy(output, EncodePolicy::Auto);
173
174        SixelError::from_status(sixel_dither_new(dither_ptr, 256, ptr::null_mut()))?;
175
176        SixelError::from_status(sixel_dither_initialize(
177            dither,
178            pixels,
179            width,
180            height,
181            pixelformat,
182            MethodForLargest::Auto,
183            sixel_sys::MethodForRepColor::Auto,
184            sixel_sys::QualityMode::Auto,
185        ))?;
186        sixel_dither_set_pixelformat(dither, pixelformat);
187        sixel_dither_set_diffusion_type(dither, method_for_diffuse);
188
189        SixelError::from_status(sixel_encode(
190            pixels,
191            width,
192            height,
193            DEPTH_ALWAYS_IGNORED,
194            dither,
195            output,
196        ))?;
197
198        sixel_output_destroy(output);
199        sixel_dither_destroy(dither);
200
201        // TODO: should we just return something like [u8]? Is all sixel data valid utf8?
202        String::from_utf8(mem::transmute(sixel_data)).map_err(SixelError::Utf8)
203    }
204}