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}