use std::fs::File;
use std::io::{BufWriter, Seek, Write};
use std::path::Path;
use zip::write::FileOptions;
use zip::{CompressionMethod, ZipWriter};
use super::IoError;
use crate::EventStream;
const SECONDS_DECIMALS: usize = 6;
enum Sink {
Text(BufWriter<File>),
Zip(Box<ZipWriter<BufWriter<File>>>),
}
impl Write for Sink {
fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
match self {
Self::Text(writer) => writer.write(buffer),
Self::Zip(writer) => writer.write(buffer),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
Self::Text(writer) => writer.flush(),
Self::Zip(writer) => writer.flush(),
}
}
}
pub struct E2vidWriter {
sink: Sink,
sensor_size: Option<(usize, usize)>,
last_t_us: Option<i64>,
n_events: usize,
}
impl E2vidWriter {
pub fn create(path: impl AsRef<Path>) -> Result<Self, IoError> {
let path = path.as_ref();
let file = BufWriter::new(File::create(path).map_err(IoError::Io)?);
let zipped = path
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("zip"));
let sink = if zipped {
let stem = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("events");
let mut writer = ZipWriter::new(file);
writer
.start_file(
format!("{stem}.txt"),
FileOptions::default().compression_method(CompressionMethod::Deflated),
)
.map_err(zip_error)?;
Sink::Zip(Box::new(writer))
} else {
Sink::Text(file)
};
Ok(Self {
sink,
sensor_size: None,
last_t_us: None,
n_events: 0,
})
}
pub fn append(&mut self, stream: &EventStream) -> Result<(), IoError> {
let sensor_size = stream.sensor_size();
match self.sensor_size {
None => {
let (width, height) = sensor_size;
writeln!(self.sink, "{width} {height}").map_err(IoError::Io)?;
self.sensor_size = Some(sensor_size);
}
Some(first) if first != sensor_size => {
return Err(IoError::Unsupported(format!(
"an E2VID file holds one recording: this append is {}x{} but the file was \
started as {}x{}",
sensor_size.0, sensor_size.1, first.0, first.1
)))
}
Some(_) => {}
}
let seconds_per_unit = stream.timestamp_scale_ms() / 1000.0;
let (xs, ys, ts, ps) = (stream.xs(), stream.ys(), stream.ts(), stream.ps());
for index in 0..stream.len() {
let t = ts[index];
if self.last_t_us.is_some_and(|last| t < last) {
return Err(IoError::Unsupported(format!(
"E2VID needs events in time order, but timestamp {t} follows {}; sort the \
stream first (stream.sort_by_time())",
self.last_t_us.unwrap_or_default()
)));
}
self.last_t_us = Some(t);
writeln!(
self.sink,
"{:.*} {} {} {}",
SECONDS_DECIMALS,
t as f64 * seconds_per_unit,
xs[index],
ys[index],
u8::from(ps[index])
)
.map_err(IoError::Io)?;
}
self.n_events += stream.len();
Ok(())
}
pub fn n_events(&self) -> usize {
self.n_events
}
pub fn finish(mut self) -> Result<(), IoError> {
if self.sensor_size.is_none() {
writeln!(self.sink, "0 0").map_err(IoError::Io)?;
}
match self.sink {
Sink::Text(mut writer) => writer.flush().map_err(IoError::Io),
Sink::Zip(mut writer) => {
let mut file = writer.finish().map_err(zip_error)?;
file.flush().map_err(IoError::Io)
}
}
}
}
pub fn write_e2vid(path: impl AsRef<Path>, stream: &EventStream) -> Result<(), IoError> {
let mut writer = E2vidWriter::create(path)?;
writer.append(stream)?;
writer.finish()
}
fn zip_error(error: zip::result::ZipError) -> IoError {
match error {
zip::result::ZipError::Io(error) => IoError::Io(error),
other => IoError::Format(other.to_string()),
}
}
impl Seek for Sink {
fn seek(&mut self, position: std::io::SeekFrom) -> std::io::Result<u64> {
match self {
Self::Text(writer) => writer.seek(position),
Self::Zip(_) => Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"a zipped E2VID entry is written sequentially",
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::EventStreamBuilder;
fn stream() -> EventStream {
let mut builder = EventStreamBuilder::new(640, 480, 0.001);
builder.push(10, 20, 1_500_000, true);
builder.push(11, 21, 2_250_000, false);
builder.build()
}
fn written(name: &str, stream: &EventStream) -> String {
let path = std::env::temp_dir().join(name);
write_e2vid(&path, stream).unwrap();
let text = std::fs::read_to_string(&path).unwrap();
std::fs::remove_file(&path).ok();
text
}
#[test]
fn writes_the_header_then_seconds_x_y_polarity() {
let text = written("eventcv_e2vid_basic.txt", &stream());
let mut lines = text.lines();
assert_eq!(lines.next(), Some("640 480"));
assert_eq!(lines.next(), Some("1.500000 10 20 1"));
assert_eq!(lines.next(), Some("2.250000 11 21 0"));
assert_eq!(lines.next(), None);
}
#[test]
fn appends_keep_one_header_and_count_events() {
let path = std::env::temp_dir().join("eventcv_e2vid_append.txt");
let mut writer = E2vidWriter::create(&path).unwrap();
writer.append(&stream()).unwrap();
writer.append(&stream().time_shift(10_000_000)).unwrap();
assert_eq!(writer.n_events(), 4);
writer.finish().unwrap();
let text = std::fs::read_to_string(&path).unwrap();
std::fs::remove_file(&path).ok();
assert_eq!(text.lines().filter(|line| *line == "640 480").count(), 1);
assert_eq!(text.lines().count(), 5);
}
#[test]
fn an_empty_stream_still_writes_a_loadable_file() {
let empty = EventStreamBuilder::new(346, 260, 0.001).build();
let text = written("eventcv_e2vid_empty.txt", &empty);
assert_eq!(text, "346 260\n");
}
#[test]
fn out_of_order_timestamps_are_rejected() {
let mut builder = EventStreamBuilder::new(64, 64, 0.001);
builder.push(1, 1, 2_000_000, true);
builder.push(1, 1, 1_000_000, true);
let path = std::env::temp_dir().join("eventcv_e2vid_unsorted.txt");
let error = write_e2vid(&path, &builder.build()).unwrap_err();
std::fs::remove_file(&path).ok();
assert!(error.to_string().contains("time order"), "{error}");
}
#[test]
fn a_second_sensor_size_is_rejected() {
let path = std::env::temp_dir().join("eventcv_e2vid_mixed.txt");
let mut writer = E2vidWriter::create(&path).unwrap();
writer.append(&stream()).unwrap();
let other = EventStreamBuilder::new(346, 260, 0.001).build();
let error = writer.append(&other).unwrap_err();
writer.finish().ok();
std::fs::remove_file(&path).ok();
assert!(error.to_string().contains("one recording"), "{error}");
}
#[test]
fn a_zip_target_holds_one_named_text_entry() {
let path = std::env::temp_dir().join("eventcv_e2vid_zipped.zip");
write_e2vid(&path, &stream()).unwrap();
let file = File::open(&path).unwrap();
let mut archive = zip::ZipArchive::new(file).unwrap();
assert_eq!(archive.len(), 1);
let mut entry = archive.by_index(0).unwrap();
assert_eq!(entry.name(), "eventcv_e2vid_zipped.txt");
let mut text = String::new();
std::io::Read::read_to_string(&mut entry, &mut text).unwrap();
drop(entry);
drop(archive);
std::fs::remove_file(&path).ok();
assert!(text.starts_with("640 480\n1.500000 10 20 1\n"), "{text}");
}
#[test]
fn a_non_microsecond_stream_still_writes_seconds() {
let mut builder = EventStreamBuilder::new(64, 64, 1.0); builder.push(1, 1, 1500, true);
let text = written("eventcv_e2vid_scale.txt", &builder.build());
assert!(text.contains("1.500000 1 1 1"), "{text}");
}
}