use std::borrow::Cow;
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
use std::ptr;
use std::slice;
use std::sync::{Mutex, OnceLock};
use crate::color_scheme::default_color_scheme_data;
use crate::heatmap::Heatmap;
use crate::magnitude::{Magnitude, MagnitudeMapped};
use crate::spectrum::{BarStyle, Spectrum};
use crate::write_png::encode_png;
#[repr(C)]
pub struct PlotpxBuffer {
pub data: *mut u8,
pub len: usize,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
pub struct PlotpxHeatmapPoint {
pub x: u32,
pub y: u32,
pub weight: f32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub enum PlotpxSpectrumStyle {
Solid = 0,
Gradient = 1,
Segmented = 2,
}
impl Default for PlotpxSpectrumStyle {
fn default() -> Self {
PlotpxSpectrumStyle::Solid
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct PlotpxSpectrumConfig {
pub width: u32,
pub height: u32,
pub style: PlotpxSpectrumStyle,
pub show_peaks: bool,
pub peak_decay: f32,
pub bar_width_factor: f32,
pub background: [u8; 4],
}
impl Default for PlotpxSpectrumConfig {
fn default() -> Self {
Self {
width: 0,
height: 0,
style: PlotpxSpectrumStyle::Solid,
show_peaks: false,
peak_decay: 0.0,
bar_width_factor: 0.8,
background: [0, 0, 0, 0],
}
}
}
static LAST_ERROR: OnceLock<Mutex<Option<CString>>> = OnceLock::new();
fn last_error_slot() -> &'static Mutex<Option<CString>> {
LAST_ERROR.get_or_init(|| Mutex::new(None))
}
fn clear_last_error() {
if let Ok(mut slot) = last_error_slot().lock() {
*slot = None;
}
}
fn set_last_error(message: impl Into<String>) -> c_int {
let message = message.into();
let cstring = CString::new(message).unwrap_or_else(|_| {
CString::new("plotpx: error message contained interior NUL byte").unwrap()
});
if let Ok(mut slot) = last_error_slot().lock() {
*slot = Some(cstring);
}
-1
}
fn resolve_colors<'a>(colors: Option<&'a [u8]>) -> Result<Cow<'a, [u8]>, String> {
match colors {
Some(custom) => {
if custom.len() % 4 != 0 {
Err("color buffer length must be a multiple of 4".to_string())
} else {
Ok(Cow::Borrowed(custom))
}
}
None => Ok(Cow::Owned(default_color_scheme_data())),
}
}
fn render_magnitude_png(
data: &[f32],
width: u32,
height: u32,
saturation: Option<f32>,
colors: Option<&[u8]>,
) -> Result<Vec<u8>, String> {
if width == 0 || height == 0 {
return Err("width and height must be greater than zero".to_string());
}
let expected_len = width as usize * height as usize;
if data.len() != expected_len {
return Err(format!(
"data length mismatch: expected {} elements for {}x{} image, got {}",
expected_len,
width,
height,
data.len()
));
}
let mut magnitude = Magnitude::new(width, height);
if expected_len == 0 {
magnitude.max_magnitude = 0.0;
magnitude.min_magnitude = 0.0;
} else {
let mut min_value = f32::INFINITY;
let mut max_value = f32::NEG_INFINITY;
let mut saw_finite = false;
for (dst, &src) in magnitude.buffer.iter_mut().zip(data.iter()) {
let value = if src.is_finite() { src } else { 0.0 };
*dst = value;
if value.is_finite() {
saw_finite = true;
if value < min_value {
min_value = value;
}
if value > max_value {
max_value = value;
}
}
}
if saw_finite {
magnitude.min_magnitude = min_value;
magnitude.max_magnitude = max_value;
} else {
magnitude.min_magnitude = 0.0;
magnitude.max_magnitude = 0.0;
}
}
let colors_storage = resolve_colors(colors)?;
let colors_slice = colors_storage.as_ref();
let rgba = if let Some(sat) = saturation {
if !(sat.is_finite() && sat > 0.0) {
return Err("saturation must be positive and finite".to_string());
}
magnitude.shift_buffer_to_non_negative();
magnitude.render_saturated(colors_slice, sat)
} else {
magnitude.render_with_colors(colors_slice)
};
encode_png(&rgba, width, height).map_err(|err| err.to_string())
}
fn render_magnitude_mapped_png(
data: &[f32],
input_width: u32,
input_height: u32,
image_width: u32,
image_height: u32,
saturation: Option<f32>,
colors: Option<&[u8]>,
) -> Result<Vec<u8>, String> {
if input_width == 0 || input_height == 0 {
return Err("input dimensions must be greater than zero".to_string());
}
if image_width == 0 || image_height == 0 {
return Err("image dimensions must be greater than zero".to_string());
}
let expected_len = input_width as usize * input_height as usize;
if data.len() != expected_len {
return Err(format!(
"data length mismatch: expected {} elements for {}x{} grid, got {}",
expected_len,
input_width,
input_height,
data.len()
));
}
let mut plot = MagnitudeMapped::new(input_width, input_height, image_width, image_height);
let mut saw_finite = false;
for (idx, &value) in data.iter().enumerate() {
let x = (idx as u32) % input_width;
let y = (idx as u32) / input_width;
let sanitized = if value.is_finite() { value } else { 0.0 };
if value.is_finite() {
saw_finite = true;
}
plot.add_point(x, y, sanitized);
}
if !saw_finite {
plot.max_magnitude = 0.0;
plot.min_magnitude = 0.0;
}
let colors_storage = resolve_colors(colors)?;
let colors_slice = colors_storage.as_ref();
let rgba = if let Some(sat) = saturation {
if !(sat.is_finite() && sat > 0.0) {
return Err("saturation must be positive and finite".to_string());
}
plot.shift_buffer_to_non_negative();
plot.render_saturated(colors_slice, sat)
} else {
plot.render_with_colors(colors_slice)
};
encode_png(&rgba, image_width, image_height).map_err(|err| err.to_string())
}
fn render_heatmap_png(
points: &[PlotpxHeatmapPoint],
width: u32,
height: u32,
saturation: Option<f32>,
colors: Option<&[u8]>,
) -> Result<Vec<u8>, String> {
if width == 0 || height == 0 {
return Err("width and height must be greater than zero".to_string());
}
let mut heatmap = Heatmap::new(width, height);
for point in points {
if point.x >= width || point.y >= height {
return Err(format!(
"point ({}, {}) was outside heatmap bounds {}x{}",
point.x, point.y, width, height
));
}
if !point.weight.is_finite() {
return Err("point weight must be finite".to_string());
}
if point.weight < 0.0 {
return Err("point weight must be non-negative".to_string());
}
if point.weight == 0.0 {
continue;
}
heatmap.add_weighted_point(point.x, point.y, point.weight);
}
let colors_storage = resolve_colors(colors)?;
let colors_slice = colors_storage.as_ref();
let rgba = if let Some(sat) = saturation {
if !(sat.is_finite() && sat > 0.0) {
return Err("saturation must be positive and finite".to_string());
}
heatmap.render_saturated(colors_slice, sat)
} else {
heatmap.render_with_colors(colors_slice)
};
encode_png(&rgba, width, height).map_err(|err| err.to_string())
}
fn render_spectrum_png(
magnitudes: &[f32],
config: &PlotpxSpectrumConfig,
saturation: Option<f32>,
colors: Option<&[u8]>,
) -> Result<Vec<u8>, String> {
if config.width == 0 || config.height == 0 {
return Err("spectrum width and height must be greater than zero".to_string());
}
if magnitudes.is_empty() {
return Err("spectrum requires at least one magnitude bin".to_string());
}
if !(config.peak_decay.is_finite() && config.peak_decay >= 0.0) {
return Err("peak_decay must be finite and non-negative".to_string());
}
if !config.bar_width_factor.is_finite() {
return Err("bar_width_factor must be finite".to_string());
}
let mut spectrum = Spectrum::new(magnitudes.len() as u32, config.width, config.height);
spectrum.style = match config.style {
PlotpxSpectrumStyle::Solid => BarStyle::Solid,
PlotpxSpectrumStyle::Gradient => BarStyle::Gradient,
PlotpxSpectrumStyle::Segmented => BarStyle::Segmented,
};
spectrum.show_peaks = config.show_peaks;
spectrum.peak_decay = config.peak_decay;
spectrum.bar_width_factor = config.bar_width_factor.clamp(0.05, 1.0);
spectrum.set_background_color(
config.background[0],
config.background[1],
config.background[2],
config.background[3],
);
let mut sanitized = Vec::with_capacity(magnitudes.len());
let mut saw_finite = false;
for &value in magnitudes {
let sanitized_value = if value.is_finite() { value } else { 0.0 };
if value.is_finite() {
saw_finite = true;
}
sanitized.push(sanitized_value);
}
spectrum.update(&sanitized);
if !saw_finite {
spectrum.max_magnitude = 0.0;
spectrum.min_magnitude = 0.0;
if spectrum.show_peaks {
spectrum.peak_values.fill(0.0);
}
}
let colors_storage = resolve_colors(colors)?;
let colors_slice = colors_storage.as_ref();
let rgba = if let Some(sat) = saturation {
if !(sat.is_finite() && sat > 0.0) {
return Err("saturation must be positive and finite".to_string());
}
spectrum.shift_buffer_to_non_negative();
spectrum.render_saturated(colors_slice, sat)
} else {
spectrum.render_with_colors(colors_slice)
};
encode_png(&rgba, config.width, config.height).map_err(|err| err.to_string())
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_magnitude_png_buffer(
data_ptr: *const f32,
data_len: usize,
width: u32,
height: u32,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
out_buffer: *mut PlotpxBuffer,
) -> c_int {
if out_buffer.is_null() {
return set_last_error("out_buffer pointer was null");
}
(*out_buffer).data = ptr::null_mut();
(*out_buffer).len = 0;
if data_ptr.is_null() {
return set_last_error("data pointer was null");
}
let data_slice = slice::from_raw_parts(data_ptr, data_len);
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_magnitude_png(data_slice, width, height, saturation_opt, colors_slice) {
Ok(png_bytes) => {
let mut boxed = png_bytes.into_boxed_slice();
let len = boxed.len();
let data = if len == 0 {
ptr::null_mut()
} else {
let ptr = boxed.as_mut_ptr();
let _ = Box::into_raw(boxed);
ptr
};
(*out_buffer).data = data;
(*out_buffer).len = len;
clear_last_error();
0
}
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_magnitude_png_file(
path_ptr: *const c_char,
data_ptr: *const f32,
data_len: usize,
width: u32,
height: u32,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
) -> c_int {
if path_ptr.is_null() {
return set_last_error("path pointer was null");
}
let path_cstr = match CStr::from_ptr(path_ptr).to_str() {
Ok(s) => s,
Err(_) => return set_last_error("path was not valid UTF-8"),
};
if data_ptr.is_null() {
return set_last_error("data pointer was null");
}
let data_slice = slice::from_raw_parts(data_ptr, data_len);
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_magnitude_png(data_slice, width, height, saturation_opt, colors_slice) {
Ok(png_bytes) => match std::fs::write(path_cstr, png_bytes) {
Ok(_) => {
clear_last_error();
0
}
Err(err) => set_last_error(err.to_string()),
},
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_magnitude_mapped_png_buffer(
data_ptr: *const f32,
data_len: usize,
input_width: u32,
input_height: u32,
image_width: u32,
image_height: u32,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
out_buffer: *mut PlotpxBuffer,
) -> c_int {
if out_buffer.is_null() {
return set_last_error("out_buffer pointer was null");
}
(*out_buffer).data = ptr::null_mut();
(*out_buffer).len = 0;
if data_ptr.is_null() {
return set_last_error("data pointer was null");
}
let data_slice = slice::from_raw_parts(data_ptr, data_len);
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_magnitude_mapped_png(
data_slice,
input_width,
input_height,
image_width,
image_height,
saturation_opt,
colors_slice,
) {
Ok(png_bytes) => {
let mut boxed = png_bytes.into_boxed_slice();
let len = boxed.len();
let data = if len == 0 {
ptr::null_mut()
} else {
let ptr = boxed.as_mut_ptr();
let _ = Box::into_raw(boxed);
ptr
};
(*out_buffer).data = data;
(*out_buffer).len = len;
clear_last_error();
0
}
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_magnitude_mapped_png_file(
path_ptr: *const c_char,
data_ptr: *const f32,
data_len: usize,
input_width: u32,
input_height: u32,
image_width: u32,
image_height: u32,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
) -> c_int {
if path_ptr.is_null() {
return set_last_error("path pointer was null");
}
let path_cstr = match CStr::from_ptr(path_ptr).to_str() {
Ok(s) => s,
Err(_) => return set_last_error("path was not valid UTF-8"),
};
if data_ptr.is_null() {
return set_last_error("data pointer was null");
}
let data_slice = slice::from_raw_parts(data_ptr, data_len);
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_magnitude_mapped_png(
data_slice,
input_width,
input_height,
image_width,
image_height,
saturation_opt,
colors_slice,
) {
Ok(png_bytes) => match std::fs::write(path_cstr, png_bytes) {
Ok(_) => {
clear_last_error();
0
}
Err(err) => set_last_error(err.to_string()),
},
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_heatmap_png_buffer(
points_ptr: *const PlotpxHeatmapPoint,
points_len: usize,
width: u32,
height: u32,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
out_buffer: *mut PlotpxBuffer,
) -> c_int {
if out_buffer.is_null() {
return set_last_error("out_buffer pointer was null");
}
(*out_buffer).data = ptr::null_mut();
(*out_buffer).len = 0;
if points_ptr.is_null() && points_len != 0 {
return set_last_error("points pointer was null");
}
let points_slice = if points_len == 0 {
&[]
} else {
slice::from_raw_parts(points_ptr, points_len)
};
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_heatmap_png(points_slice, width, height, saturation_opt, colors_slice) {
Ok(png_bytes) => {
let mut boxed = png_bytes.into_boxed_slice();
let len = boxed.len();
let data = if len == 0 {
ptr::null_mut()
} else {
let ptr = boxed.as_mut_ptr();
let _ = Box::into_raw(boxed);
ptr
};
(*out_buffer).data = data;
(*out_buffer).len = len;
clear_last_error();
0
}
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_heatmap_png_file(
path_ptr: *const c_char,
points_ptr: *const PlotpxHeatmapPoint,
points_len: usize,
width: u32,
height: u32,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
) -> c_int {
if path_ptr.is_null() {
return set_last_error("path pointer was null");
}
let path_cstr = match CStr::from_ptr(path_ptr).to_str() {
Ok(s) => s,
Err(_) => return set_last_error("path was not valid UTF-8"),
};
if points_ptr.is_null() && points_len != 0 {
return set_last_error("points pointer was null");
}
let points_slice = if points_len == 0 {
&[]
} else {
slice::from_raw_parts(points_ptr, points_len)
};
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_heatmap_png(points_slice, width, height, saturation_opt, colors_slice) {
Ok(png_bytes) => match std::fs::write(path_cstr, png_bytes) {
Ok(_) => {
clear_last_error();
0
}
Err(err) => set_last_error(err.to_string()),
},
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_spectrum_png_buffer(
data_ptr: *const f32,
data_len: usize,
config_ptr: *const PlotpxSpectrumConfig,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
out_buffer: *mut PlotpxBuffer,
) -> c_int {
if out_buffer.is_null() {
return set_last_error("out_buffer pointer was null");
}
(*out_buffer).data = ptr::null_mut();
(*out_buffer).len = 0;
if data_ptr.is_null() {
return set_last_error("data pointer was null");
}
if config_ptr.is_null() {
return set_last_error("config pointer was null");
}
let data_slice = slice::from_raw_parts(data_ptr, data_len);
let config = *config_ptr;
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_spectrum_png(data_slice, &config, saturation_opt, colors_slice) {
Ok(png_bytes) => {
let mut boxed = png_bytes.into_boxed_slice();
let len = boxed.len();
let data = if len == 0 {
ptr::null_mut()
} else {
let ptr = boxed.as_mut_ptr();
let _ = Box::into_raw(boxed);
ptr
};
(*out_buffer).data = data;
(*out_buffer).len = len;
clear_last_error();
0
}
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_write_spectrum_png_file(
path_ptr: *const c_char,
data_ptr: *const f32,
data_len: usize,
config_ptr: *const PlotpxSpectrumConfig,
saturation: f32,
color_ptr: *const u8,
color_len: usize,
) -> c_int {
if path_ptr.is_null() {
return set_last_error("path pointer was null");
}
let path_cstr = match CStr::from_ptr(path_ptr).to_str() {
Ok(s) => s,
Err(_) => return set_last_error("path was not valid UTF-8"),
};
if data_ptr.is_null() {
return set_last_error("data pointer was null");
}
if config_ptr.is_null() {
return set_last_error("config pointer was null");
}
let data_slice = slice::from_raw_parts(data_ptr, data_len);
let config = *config_ptr;
let colors_slice = if color_ptr.is_null() || color_len == 0 {
None
} else {
Some(slice::from_raw_parts(color_ptr, color_len))
};
let saturation_opt = if saturation > 0.0 {
Some(saturation)
} else {
None
};
match render_spectrum_png(data_slice, &config, saturation_opt, colors_slice) {
Ok(png_bytes) => match std::fs::write(path_cstr, png_bytes) {
Ok(_) => {
clear_last_error();
0
}
Err(err) => set_last_error(err.to_string()),
},
Err(err) => set_last_error(err),
}
}
#[no_mangle]
pub extern "C" fn plotpx_last_error_message() -> *const c_char {
if let Ok(slot) = last_error_slot().lock() {
if let Some(err) = slot.as_ref() {
return err.as_ptr();
}
}
ptr::null()
}
#[no_mangle]
pub unsafe extern "C" fn plotpx_free_buffer(buffer: PlotpxBuffer) {
if buffer.data.is_null() || buffer.len == 0 {
return;
}
let slice_ptr = ptr::slice_from_raw_parts_mut(buffer.data, buffer.len);
drop(Box::from_raw(slice_ptr));
}
#[cfg(test)]
mod tests {
use super::*;
use png::Decoder;
use std::ffi::{CStr, CString};
use std::fs;
use std::ptr;
const PNG_SIGNATURE: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
fn decode_dimensions(bytes: &[u8]) -> (u32, u32, Vec<u8>) {
let decoder = Decoder::new(bytes);
let mut reader = decoder.read_info().expect("invalid png");
let mut buffer = vec![0u8; reader.output_buffer_size()];
let info = reader
.next_frame(&mut buffer)
.expect("failed to read png frame");
(
info.width,
info.height,
buffer[..info.buffer_size()].to_vec(),
)
}
#[test]
fn buffer_roundtrip_default_colors() {
let width = 4;
let height = 2;
let data = [0.0f32, 0.5, 1.0, 0.25, 0.1, 0.2, 0.3, 0.4];
let mut buffer = PlotpxBuffer {
data: ptr::null_mut(),
len: 0,
};
let code = unsafe {
plotpx_write_magnitude_png_buffer(
data.as_ptr(),
data.len(),
width,
height,
0.0,
ptr::null(),
0,
&mut buffer,
)
};
assert_eq!(code, 0);
assert!(!buffer.data.is_null());
assert!(buffer.len > PNG_SIGNATURE.len());
let png_bytes = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len).to_vec() };
assert!(png_bytes.starts_with(&PNG_SIGNATURE));
unsafe { plotpx_free_buffer(buffer) };
let (png_width, png_height, _) = decode_dimensions(&png_bytes);
assert_eq!(png_width, width);
assert_eq!(png_height, height);
}
#[test]
fn buffer_roundtrip_custom_colors_with_saturation() {
let width = 2;
let height = 2;
let data = [0.0f32, 0.5, 1.0, 0.75];
let colors: [u8; 8] = [0, 0, 0, 255, 255, 0, 0, 255];
let mut buffer = PlotpxBuffer {
data: ptr::null_mut(),
len: 0,
};
let code = unsafe {
plotpx_write_magnitude_png_buffer(
data.as_ptr(),
data.len(),
width,
height,
1.0,
colors.as_ptr(),
colors.len(),
&mut buffer,
)
};
assert_eq!(code, 0);
assert!(!buffer.data.is_null());
let png_bytes = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len).to_vec() };
unsafe { plotpx_free_buffer(buffer) };
let (png_width, png_height, pixels) = decode_dimensions(&png_bytes);
assert_eq!(png_width, width);
assert_eq!(png_height, height);
for chunk in pixels.chunks_exact(4) {
assert!(
chunk == &colors[0..4] || chunk == &colors[4..8],
"unexpected pixel {:?}",
chunk
);
}
}
#[test]
fn length_mismatch_reports_error() {
let data = [0.0f32; 3];
let mut buffer = PlotpxBuffer {
data: ptr::null_mut(),
len: 0,
};
let code = unsafe {
plotpx_write_magnitude_png_buffer(
data.as_ptr(),
data.len(),
2,
2,
0.0,
ptr::null(),
0,
&mut buffer,
)
};
assert_eq!(code, -1);
assert!(buffer.data.is_null());
assert_eq!(buffer.len, 0);
let message_ptr = plotpx_last_error_message();
assert!(!message_ptr.is_null());
let message = unsafe { CStr::from_ptr(message_ptr) };
let message = message.to_string_lossy();
assert!(message.contains("data length mismatch"), "{}", message);
}
#[test]
fn file_output_creates_png_on_disk() {
let temp_dir = tempfile::tempdir().expect("tempdir");
let path = temp_dir.path().join("plotpx_test.png");
let path_str = path.to_str().expect("utf-8 path");
let path_cstr = CString::new(path_str).expect("c string");
let data = [0.1f32; 4];
let code = unsafe {
plotpx_write_magnitude_png_file(
path_cstr.as_ptr(),
data.as_ptr(),
data.len(),
2,
2,
0.0,
ptr::null(),
0,
)
};
assert_eq!(code, 0);
let contents = fs::read(&path).expect("png exists");
assert!(contents.starts_with(&PNG_SIGNATURE));
assert!(contents.len() > PNG_SIGNATURE.len());
}
#[test]
fn magnitude_mapped_roundtrip() {
let input_width = 2;
let input_height = 2;
let image_width = 4;
let image_height = 4;
let data = [0.0f32, 0.5, 1.0, 0.25];
let mut buffer = PlotpxBuffer {
data: ptr::null_mut(),
len: 0,
};
let code = unsafe {
plotpx_write_magnitude_mapped_png_buffer(
data.as_ptr(),
data.len(),
input_width,
input_height,
image_width,
image_height,
0.0,
ptr::null(),
0,
&mut buffer,
)
};
assert_eq!(code, 0);
assert!(!buffer.data.is_null());
let png_bytes = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len).to_vec() };
unsafe { plotpx_free_buffer(buffer) };
let (png_width, png_height, _) = decode_dimensions(&png_bytes);
assert_eq!(png_width, image_width);
assert_eq!(png_height, image_height);
}
#[test]
fn heatmap_roundtrip() {
let points = [
PlotpxHeatmapPoint {
x: 2,
y: 2,
weight: 1.0,
},
PlotpxHeatmapPoint {
x: 5,
y: 3,
weight: 0.5,
},
];
let mut buffer = PlotpxBuffer {
data: ptr::null_mut(),
len: 0,
};
let code = unsafe {
plotpx_write_heatmap_png_buffer(
points.as_ptr(),
points.len(),
8,
6,
0.0,
ptr::null(),
0,
&mut buffer,
)
};
assert_eq!(code, 0);
assert!(!buffer.data.is_null());
let png_bytes = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len).to_vec() };
unsafe { plotpx_free_buffer(buffer) };
let (png_width, png_height, _) = decode_dimensions(&png_bytes);
assert_eq!(png_width, 8);
assert_eq!(png_height, 6);
}
#[test]
fn spectrum_roundtrip() {
let data = [0.1f32, 0.5, 0.9, 0.3, 0.7];
let config = PlotpxSpectrumConfig {
width: 32,
height: 24,
style: PlotpxSpectrumStyle::Gradient,
show_peaks: true,
peak_decay: 0.1,
bar_width_factor: 0.9,
background: [5, 10, 15, 255],
};
let mut buffer = PlotpxBuffer {
data: ptr::null_mut(),
len: 0,
};
let code = unsafe {
plotpx_write_spectrum_png_buffer(
data.as_ptr(),
data.len(),
&config,
0.0,
ptr::null(),
0,
&mut buffer,
)
};
assert_eq!(code, 0);
assert!(!buffer.data.is_null());
let png_bytes = unsafe { std::slice::from_raw_parts(buffer.data, buffer.len).to_vec() };
unsafe { plotpx_free_buffer(buffer) };
let (png_width, png_height, _) = decode_dimensions(&png_bytes);
assert_eq!(png_width, config.width);
assert_eq!(png_height, config.height);
}
}