250 lines
7.3 KiB
Rust
250 lines
7.3 KiB
Rust
// Name: bomp
|
|
// Description: bitmap plotter utility
|
|
// Why: generate viewable plots from f32be data files
|
|
// Processor usage: low
|
|
// Memory usage: loads all input files into memory at once
|
|
|
|
#![allow(unused_parens)]
|
|
|
|
use std::io::prelude::*;
|
|
use std::fs::File;
|
|
use std::env::args;
|
|
use std::process::exit;
|
|
|
|
static OUTER_PADDING:usize = 16;
|
|
static INTERPLOT_PADDING:usize = 16;
|
|
static SIZE_ROUNDOFF:usize = 16;
|
|
|
|
fn main()
|
|
{
|
|
let argv = args().collect::<Vec<String>>();
|
|
if argv.len() < 4
|
|
{
|
|
eprintln!("usage: bomp <yscale> <output.bmp> <input.dat> [<optional additional inputs>...]");
|
|
exit(1);
|
|
}
|
|
|
|
/*
|
|
yscale (integer):
|
|
the vertical scale of each plot, equal to the distance in pixels from zero to the plot's max value.
|
|
plots are thus 2*yscale+1 pixels high, plus padding.
|
|
|
|
output.bmp (bitmap file):
|
|
a classic microsoft bitmap file to which to write the uncompressed plot image.
|
|
if you want to use it elsewhere, i recommend converting it via ffmpeg to png.
|
|
|
|
inputs.dat (raw big-endian f32 data file):
|
|
simply a sequence of arbitrarily many f32 values in big-endian byte order.
|
|
you can specify an arbitrary number of input files, each of which will produce
|
|
its own parallel plot below the previous one on the resulting image. parallel plots
|
|
will use the same horizontal scale (always exactly one pixel per data value) and
|
|
vertical scale (automatically adjusted so that the overall maximum value between all
|
|
data sets matches the specified yscale value in pixel height).
|
|
*/
|
|
|
|
let yscale = match argv[1].parse::<usize>()
|
|
{
|
|
Ok(n) => n,
|
|
Err(_) =>
|
|
{
|
|
eprintln!("bomp: the first command-line argument needs to be an integer (number of vertical pixels per plot.)");
|
|
exit(1);
|
|
},
|
|
};
|
|
|
|
let output_filename:String = argv[2].clone();
|
|
let mut output_file = match File::create(&output_filename)
|
|
{
|
|
Ok(file) => file,
|
|
Err(why) =>
|
|
{
|
|
eprintln!("bomp: could not open {} for writing: {}", output_filename, why);
|
|
exit(1);
|
|
},
|
|
};
|
|
|
|
let mut input_files:Vec<(File, String)> = Vec::new();
|
|
for i in 3..argv.len()
|
|
{
|
|
let new_filename:String = argv[i].clone();
|
|
let new_file = match File::open(&argv[i])
|
|
{
|
|
Err(reason) =>
|
|
{
|
|
eprintln!("bomp: failed to open {} for reading: {}", new_filename, reason);
|
|
exit(1);
|
|
},
|
|
Ok(file) => file,
|
|
};
|
|
|
|
input_files.push((new_file, new_filename));
|
|
}
|
|
|
|
let mut signals:Vec<Vec<f32>> = Vec::new();
|
|
for (file,filename) in input_files.iter_mut()
|
|
{
|
|
let mut file_buffer:Vec<u8> = Vec::new();
|
|
match file.read_to_end(&mut file_buffer)
|
|
{
|
|
Err(reason) =>
|
|
{
|
|
eprintln!("bomp: could not read from input file {}: {}", filename, reason);
|
|
exit(1);
|
|
},
|
|
Ok(_) => (),
|
|
};
|
|
let mut new_signal:Vec<f32> = Vec::with_capacity(file_buffer.len()/4);
|
|
for chunk in file_buffer.chunks_exact(4)
|
|
{
|
|
let mut sample_bytes:[u8;4] = [0x00; 4];
|
|
sample_bytes.copy_from_slice(chunk);
|
|
new_signal.push(f32::from_be_bytes(sample_bytes));
|
|
}
|
|
signals.push(new_signal);
|
|
}
|
|
|
|
let mut abs_max:f32 = 0.0;
|
|
let mut signals_maxlen:usize = 0;
|
|
for signal in signals.iter()
|
|
{
|
|
signals_maxlen = signals_maxlen.max(signal.len());
|
|
for point in signal.iter()
|
|
{
|
|
abs_max = abs_max.max(point.abs());
|
|
}
|
|
}
|
|
let scale_factor = (yscale as f32) / abs_max;
|
|
eprintln!("peak: {}", abs_max);
|
|
|
|
for signal in signals.iter_mut()
|
|
{
|
|
for point in signal.iter_mut()
|
|
{
|
|
*point *= scale_factor;
|
|
}
|
|
}
|
|
|
|
let plot_height:usize = (yscale*2 + 1);
|
|
let plot_width:usize = signals_maxlen;
|
|
let signal_count:usize = signals.len();
|
|
let intersignal_count:usize = signals.len()-1;
|
|
|
|
// adjust the padding to make the image size a nice round number.
|
|
// why? i forgot, honestly. seems pretty pointless.
|
|
let mut lpad:usize = OUTER_PADDING;
|
|
let mut rpad:usize = OUTER_PADDING;
|
|
while (plot_width + lpad + rpad) % SIZE_ROUNDOFF != 0
|
|
{
|
|
if lpad <= rpad
|
|
{
|
|
lpad += 1;
|
|
} else {
|
|
rpad += 1;
|
|
}
|
|
}
|
|
|
|
let mut tpad:usize = OUTER_PADDING;
|
|
let mut bpad:usize = OUTER_PADDING;
|
|
while (plot_height*signal_count + intersignal_count*INTERPLOT_PADDING + tpad + bpad) % SIZE_ROUNDOFF != 0
|
|
{
|
|
if tpad <= bpad
|
|
{
|
|
tpad += 1;
|
|
} else {
|
|
bpad += 1;
|
|
}
|
|
}
|
|
|
|
let color_count:usize = 256;
|
|
let header_size:usize = 54 + color_count*4;
|
|
let image_width:usize = plot_width + lpad + rpad;
|
|
let image_height:usize = signal_count*plot_height + intersignal_count*INTERPLOT_PADDING + tpad + bpad;
|
|
let file_size:usize = image_height * image_width + 1078;
|
|
|
|
eprintln!("plots: {}", signal_count);
|
|
eprintln!("wide: {}", image_width);
|
|
eprintln!("high: {}", image_height);
|
|
eprintln!("size: {}", file_size);
|
|
|
|
let mut header:Vec<u8> = Vec::with_capacity(header_size);
|
|
header.extend_from_slice(&"BM".as_bytes()); // "signature"
|
|
header.extend_from_slice(&(file_size as u32).to_le_bytes()); // filesize
|
|
header.extend_from_slice(&0u32.to_le_bytes()); // forbidden field
|
|
header.extend_from_slice(&(header_size as u32).to_le_bytes()); // data offset
|
|
|
|
header.extend_from_slice(&40u32.to_le_bytes()); // infoheader size (constant)
|
|
header.extend_from_slice(&(image_width as u32).to_le_bytes()); // pixel width
|
|
header.extend_from_slice(&(image_height as u32).to_le_bytes()); // pixel height
|
|
header.extend_from_slice(&1u16.to_le_bytes()); // number of planes
|
|
header.extend_from_slice(&8u16.to_le_bytes()); // bits per pixel
|
|
header.extend_from_slice(&0u32.to_le_bytes()); // compression type (none)
|
|
header.extend_from_slice(&0u32.to_le_bytes()); // compressed image size (not applicable)
|
|
header.extend_from_slice(&1024u32.to_le_bytes()); // horizontal pixels/meter (arbitrary)
|
|
header.extend_from_slice(&1024u32.to_le_bytes()); // vertical pixels/meter (arbitrary)
|
|
header.extend_from_slice(&256u32.to_le_bytes()); // colors used (all of them)
|
|
header.extend_from_slice(&0u32.to_le_bytes()); // number of important colors (what does that even mean)
|
|
|
|
assert_eq!(header.len(), 54);
|
|
|
|
for i in 0x00..=0xFF
|
|
{
|
|
header.push(i); // red
|
|
header.push(i); // green
|
|
header.push(i); // blue
|
|
header.push(0x00); // ?????
|
|
}
|
|
|
|
match output_file.write_all(&header)
|
|
{
|
|
Err(reason) =>
|
|
{
|
|
eprintln!("bomp: write error on file {}: {}", output_filename, reason);
|
|
exit(1);
|
|
},
|
|
Ok(_) => (),
|
|
};
|
|
|
|
output_file.write_all(&vec![0x00; bpad*image_width]).expect("write error (lower padding)");
|
|
|
|
for (signal_number, signal) in signals.iter().enumerate().rev() // reversed because bmps go from bottom to top, for some reason
|
|
{
|
|
for y in -(yscale as isize)..=(yscale as isize)
|
|
{
|
|
let mut row_buffer:Vec<u8> = Vec::with_capacity(image_width);
|
|
|
|
row_buffer.extend_from_slice(&vec![0x00; lpad]);
|
|
|
|
for x in 0..signal.len()
|
|
{
|
|
if
|
|
(
|
|
(
|
|
signal[x] >= (y as f32)
|
|
&&
|
|
y >= 0
|
|
) || (
|
|
signal[x] <= (y as f32)
|
|
&&
|
|
y <= 0
|
|
)
|
|
) {
|
|
row_buffer.push(0xFF);
|
|
} else {
|
|
row_buffer.push(0x00);
|
|
}
|
|
}
|
|
|
|
row_buffer.extend_from_slice(&vec![0x00; signals_maxlen - signal.len() + rpad]);
|
|
|
|
output_file.write_all(&row_buffer).expect("write error (row)");
|
|
}
|
|
|
|
if signal_number > 0
|
|
{
|
|
output_file.write_all(&vec![0x00; image_width*INTERPLOT_PADDING]).expect("write error (interplot padding)");
|
|
}
|
|
}
|
|
|
|
output_file.write_all(&vec![0x00; tpad*image_width]).expect("write error (upper padding)");
|
|
}
|