use nkscan::{
protocol::{caps::film::FilmFormat, decode::Image},
scan::boundaries::{self, Polarity},
};
use std::{env, path::PathBuf};
use tiff::{
decoder::{Decoder, DecodingResult, Limits, ifd::Value},
encoder::{Rational, TiffEncoder, colortype::RGB16},
tags::Tag,
};
fn resolution(decoder: &mut Decoder<impl std::io::Read + std::io::Seek>, tag: Tag) -> Option<f32> {
match decoder.get_tag(tag).ok()? {
Value::Rational(n, d) if d != 0 => Some(n as f32 / d as f32),
Value::Float(v) => Some(v),
_ => None,
}
}
fn parse_format(s: &str) -> FilmFormat {
match s.to_ascii_lowercase().as_str() {
"ix240" | "aps" => FilmFormat::IX240,
"f135" | "35mm" => FilmFormat::F135,
"f135half" | "35mmhalf" => FilmFormat::F135Half,
"f16" | "16mm" => FilmFormat::F16,
"f645" | "6x45" => FilmFormat::F645,
"f66" | "6x6" => FilmFormat::F66,
"f67" | "6x7" => FilmFormat::F67,
"f68" | "6x8" => FilmFormat::F68,
"f69" | "6x9" => FilmFormat::F69,
mm => FilmFormat::Custom(mm.parse().unwrap_or_else(|_| panic!("unknown format {mm}"))),
}
}
fn parse_polarity(s: &str) -> Polarity {
match s.to_ascii_lowercase().as_str() {
"positive" | "pos" | "slide" => Polarity::Positive,
"negative" | "neg" => Polarity::Negative,
other => panic!("unknown polarity {other}"),
}
}
fn deinterleave3(chunky: &[u16]) -> Vec<Vec<u16>> {
let mut planes: Vec<Vec<u16>> = (0..3)
.map(|_| Vec::with_capacity(chunky.len() / 3))
.collect();
for pixel in chunky.chunks_exact(3) {
for (plane, &v) in planes.iter_mut().zip(pixel) {
plane.push(v);
}
}
planes
}
fn main() {
let mut args = env::args().skip(1);
let input = PathBuf::from(
args.next()
.expect("usage: detect_frames <in.tiff> [--format F] [--polarity P] [-o out.tiff]"),
);
let mut format = FilmFormat::F135;
let mut polarity = Polarity::Negative;
let mut dpi_override: Option<f32> = None;
let mut out = PathBuf::from("detected.tiff");
let rest: Vec<String> = args.collect();
let mut i = 0;
while i < rest.len() {
match rest[i].as_str() {
"--format" => {
format = parse_format(&rest[i + 1]);
i += 2;
}
"--polarity" => {
polarity = parse_polarity(&rest[i + 1]);
i += 2;
}
"--dpi" => {
dpi_override = Some(rest[i + 1].parse().unwrap());
i += 2;
}
"-o" | "--out" => {
out = PathBuf::from(&rest[i + 1]);
i += 2;
}
other => panic!("unknown arg {other}"),
}
}
let file = std::fs::File::open(&input).unwrap_or_else(|e| panic!("{}: {e}", input.display()));
let mut decoder = Decoder::new(std::io::BufReader::new(file))
.unwrap()
.with_limits(Limits::unlimited());
let (cols, rows) = decoder.dimensions().unwrap();
let dpi = dpi_override.unwrap_or_else(|| {
resolution(&mut decoder, Tag::XResolution).unwrap_or_else(|| {
panic!(
"{}: no readable XResolution tag, pass --dpi",
input.display()
)
})
});
let chunky = match decoder.read_image().unwrap() {
DecodingResult::U16(v) => v,
other => panic!("not 16-bit samples ({other:?})"),
};
let planes = deinterleave3(&chunky);
let colors: Vec<&[u16]> = planes.iter().map(Vec::as_slice).collect();
let image = Image {
colors,
ir: &[],
rows: rows as usize,
cols: cols as usize,
bits: 16,
};
let nominal = format.height_dots(dpi.round() as u16) as usize;
let found = boundaries::detect(&image, nominal, polarity);
let length = found.length;
eprintln!(
"{}: {cols}x{rows} @ {dpi:.1} dpi, format={format:?} -> nominal={nominal}px, corrected={length}px, polarity={polarity:?}",
input.display()
);
eprintln!("pitch={}px, {} frame(s):", found.pitch, found.frames.len());
for (n, &start) in found.frames.iter().enumerate() {
let end = (start + length).min(cols as usize);
eprintln!(" #{:<2} columns [{start}, {end})", n + 1);
}
let palette: [[u16; 3]; 6] = [
[65535, 0, 0],
[0, 65535, 0],
[0, 0, 65535],
[65535, 65535, 0],
[65535, 0, 65535],
[0, 65535, 65535],
];
let mut annotated = chunky.clone();
let (w, h) = (cols as usize, rows as usize);
let mut set = |x: usize, y: usize, color: [u16; 3]| {
if x < w && y < h {
let i = (y * w + x) * 3;
annotated[i] = color[0];
annotated[i + 1] = color[1];
annotated[i + 2] = color[2];
}
};
for (n, &start) in found.frames.iter().enumerate() {
let color = palette[n % palette.len()];
let end = (start + length).min(w.saturating_sub(1));
for thick in 0..3 {
for y in 0..h {
set(start + thick, y, color);
set(end.saturating_sub(thick), y, color);
}
for x in start..=end {
set(x, thick.min(h.saturating_sub(1)), color);
set(x, h.saturating_sub(1 + thick), color);
}
}
}
let file = std::fs::File::create(&out).unwrap();
let mut tiff = TiffEncoder::new(file).unwrap();
let mut img = tiff.new_image::<RGB16>(cols, rows).unwrap();
img.resolution(
tiff::tags::ResolutionUnit::Inch,
Rational {
n: dpi.round() as u32,
d: 1,
},
);
img.write_data(&annotated).unwrap();
eprintln!("wrote {}", out.display());
}