use super::{framing, pass::Pass, strip, window};
use crate::{
error::Error,
protocol::{
caps::{
Capabilities,
set_window::{ColorInterleaving, ScanKind, ScanMode},
},
data::{
Boundary, BoundaryType2, FramePosition, PerfInformation, PerforationInformation, Rect,
},
decode::{Image, Samples},
model::Model,
window::{Flags, Window},
},
};
use tracing::*;
const PERFORATION_MM_E4: u64 = 47_498;
const INCH_MM_E4: u64 = 254_000;
const MEASURED_QUARTERS: u64 = 16;
pub(crate) fn line_pitch(caps: &Capabilities) -> u32 {
let optical =
u32::from(caps.address.y_axis.optical_dpi).max(u32::from(caps.address.x_axis.optical_dpi));
match u32::from(caps.address.thumbnail_resolution.start) {
0 => 1,
asked => (optical / asked).max(1),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LinePitch {
addresses: u64,
lines: u64,
}
impl LinePitch {
pub fn computed(caps: &Capabilities) -> Self {
Self {
addresses: u64::from(line_pitch(caps)),
lines: 1,
}
}
pub fn measured(caps: &Capabilities, perfs: &PerfInformation) -> Option<Self> {
let quarters =
|p: &PerforationInformation| u64::from(p.perf_number) * 4 + u64::from(p.perf_decimal);
let first = perfs.perfs.first()?;
let last = perfs.perfs.last()?;
let head = perfs
.perfs
.iter()
.position(|p| quarters(p) != quarters(first))?;
let tail = perfs
.perfs
.iter()
.rposition(|p| quarters(p) != quarters(last))?
+ 1;
let lines = (tail.checked_sub(head)?) as u64;
let moved =
quarters(perfs.perfs.get(tail)?).checked_sub(quarters(perfs.perfs.get(head)?))?;
if lines == 0 || moved < MEASURED_QUARTERS {
return None;
}
let optical = u64::from(
caps.address
.y_axis
.optical_dpi
.max(caps.address.x_axis.optical_dpi),
);
let addresses = moved * optical * PERFORATION_MM_E4 / (4 * INCH_MM_E4);
let measured = Self { addresses, lines };
let asked = u64::from(line_pitch(caps));
let apart = measured.addresses.abs_diff(asked * lines);
(apart * 4 <= asked * lines).then_some(measured)
}
pub fn addresses_per_column(&self) -> f64 {
self.addresses as f64 / self.lines.max(1) as f64
}
pub fn address_of(&self, line: u32) -> u32 {
(u64::from(line) * self.addresses / self.lines.max(1)) as u32
}
pub fn columns(&self, addresses: u32) -> usize {
let per = self.addresses.max(1);
((u64::from(addresses) * self.lines + per / 2) / per) as usize
}
pub fn line_at(&self, caps: &Capabilities, y: u32) -> usize {
let origin = caps.address.y_axis.address_range.start;
let film = u64::from(y.saturating_sub(origin)) * self.lines.max(1);
let addresses = self.addresses.max(1);
((film + addresses / 2) / addresses) as usize
}
}
fn top_of(caps: &Capabilities, middle: u32, format: u32) -> Option<u32> {
let origin = caps.address.y_axis.address_range.start;
let end = caps.address.y_axis.address_range.last;
let last = end.checked_sub(format)?;
Some(
middle
.saturating_sub(format / 2)
.clamp(origin.min(last), last),
)
}
pub fn available(caps: &Capabilities) -> bool {
caps.set_window.kind.contains(ScanKind::THUMBNAIL)
&& caps.address.thumbnail_resolution.start > 0
}
fn tops(
caps: &Capabilities,
image: &Image,
pitch: LinePitch,
format: u32,
) -> Option<(Vec<u32>, strip::Strip)> {
let found = strip::find(image, pitch.columns(format))?;
let origin = caps.address.y_axis.address_range.start;
let tops = found
.frames
.iter()
.map(|frame| origin + pitch.address_of((frame.start + frame.len() / 2) as u32))
.filter_map(|middle| top_of(caps, middle, format))
.collect();
Some((tops, found))
}
pub fn frames(
caps: &Capabilities,
pass: &Pass,
samples: &Samples,
length: u32,
) -> Result<(Boundary, LinePitch), Error> {
let format = window::reachable_blocks(caps, length);
framing::reachable(caps, format)?;
let image = Image::new(&pass.layout, samples)?;
let pitch = LinePitch {
addresses: u64::from(pass.layout.line_pitch.max(1)),
lines: 1,
};
let (left, width) = opening(caps);
let Some((tops, found)) = tops(caps, &image, pitch, format) else {
info!("nothing on the strip to frame");
return Ok((Boundary::default(), pitch));
};
let frames: Vec<Rect> = tops
.into_iter()
.map(|top| Rect {
top,
left,
bottom: top + format,
right: left + width,
})
.collect();
info!(
frames = frames.len(),
pitch = pitch.address_of(found.pitch as u32),
contrast = found.contrast,
"measured the loaded strip"
);
for (n, rect) in frames.iter().enumerate() {
debug!(frame = n + 1, ?rect, "frame rect");
}
Ok((Boundary { frames }, pitch))
}
pub fn frames_type2(
caps: &Capabilities,
pass: &Pass,
samples: &Samples,
perf_info: &PerfInformation,
length: u32,
) -> Result<(BoundaryType2, u32, LinePitch), Error> {
let format = window::reachable_blocks(caps, length);
framing::reachable(caps, format)?;
let image = Image::new(&pass.layout, samples)?;
let pitch = match LinePitch::measured(caps, perf_info) {
Some(measured) => {
debug!(
per_thousand_lines = measured.address_of(1000),
"measured the thumbnail line against the perforation table"
);
measured
}
None => {
debug!("no perforation table to measure the thumbnail line against");
LinePitch::computed(caps)
}
};
let Some((tops, found)) = tops(caps, &image, pitch, format) else {
info!("nothing on the strip to frame");
return Ok((BoundaryType2::default(), format, pitch));
};
if perf_info.perfs.len() != image.cols {
debug!(
perfs = perf_info.perfs.len(),
columns = image.cols,
"the perforation table does not run the length of the thumbnail pass"
);
}
let frames: Vec<FramePosition> = tops
.into_iter()
.filter_map(|top| {
let col = pitch.line_at(caps, top);
let perf = perf_info.at(col);
debug!(col, top, ?perf, "the pass over a frame");
match perf {
Some(perf) => Some(FramePosition::new(top, perf)),
None => {
warn!(col, top, "no perforation reading for this frame, dropped");
None
}
}
})
.collect();
info!(
frames = frames.len(),
pitch = pitch.address_of(found.pitch as u32),
contrast = found.contrast,
"measured the loaded strip"
);
for (n, frame) in frames.iter().enumerate() {
debug!(frame = n + 1, ?frame, "frame position");
}
Ok((BoundaryType2 { frames }, format, pitch))
}
fn opening(caps: &Capabilities) -> (u32, u32) {
let x = &caps.address.x_axis;
match caps.frames.as_ref().and_then(|f| f.images.first()) {
Some(opening) => (opening.left, opening.width),
None => (x.address_range.start, x.boundary),
}
}
pub(crate) fn windows(caps: &Capabilities) -> Result<Vec<Window>, Error> {
let y = &caps.address.y_axis;
let unsupported = |reason: String| Error::Unsupported {
op: "thumbnail window",
reason,
};
let offered = caps.set_window.interleaving;
if !offered.contains(ColorInterleaving::LINE_WITHOUT_DISTANCE) {
return Err(unsupported(format!(
"a thumbnail needs line ordering and this unit offers {offered:?}"
)));
}
let flags = match caps.identity.model() {
Some(Model::Ls8000 | Model::Ls9000) => Flags::empty(),
Some(_) => Flags::POSITIVE | Flags::AVERAGING,
None => {
return Err(Error::Unsupported {
op: "thumbnail window",
reason: "unrecognized model".into(),
});
}
};
let (left, width) = opening(caps);
let mut windows = window::blank(caps, &window::color_channels(caps))?;
for w in &mut windows {
w.resolution = (
caps.address.thumbnail_resolution.start,
caps.address.thumbnail_resolution.start,
);
w.origin = (left, y.address_range.start);
w.size = (width, y.address_range.last);
w.scanning_kind = ScanKind::THUMBNAIL;
w.scanning_mode = ScanMode::NORMAL_QUALITY;
w.flags = flags;
w.color_interleaving = ColorInterleaving::LINE_WITHOUT_DISTANCE;
}
Ok(windows)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scan::window::tests::caps;
fn table(head: usize, lines: usize, perforations: usize, tail: usize) -> PerfInformation {
let mut perfs = vec![PerforationInformation::default(); head];
for line in 0..lines * perforations {
let quarters = line * 4 / lines.max(1);
perfs.push(PerforationInformation {
perf_number: (quarters / 4) as u16,
perf_decimal: (quarters % 4) as u8,
..Default::default()
});
}
let last = perfs.last().cloned().unwrap_or_default();
perfs.extend(std::iter::repeat_n(last, tail));
PerfInformation { perfs }
}
#[test]
fn the_perforation_table_measures_the_line() {
let mut caps = caps();
caps.address.thumbnail_resolution = (97u16..=97u16).into();
assert_eq!(line_pitch(&caps), 41);
let measured = LinePitch::measured(&caps, &table(20, 18, 40, 30)).expect("a pitch");
assert_eq!(measured.address_of(1000), 41_555);
assert_eq!(
measured.address_of(137) - LinePitch::computed(&caps).address_of(137),
76
);
assert_eq!(measured.line_at(&caps, measured.address_of(137)), 137);
}
#[test]
fn an_unmeasurable_table_keeps_the_computed_pitch() {
let mut caps = caps();
caps.address.thumbnail_resolution = (97u16..=97u16).into();
assert_eq!(
LinePitch::measured(&caps, &PerfInformation::default()),
None
);
assert_eq!(LinePitch::measured(&caps, &table(40, 1, 0, 0)), None);
assert_eq!(LinePitch::measured(&caps, &table(10, 18, 3, 10)), None);
assert_eq!(LinePitch::measured(&caps, &table(10, 2, 40, 10)), None);
}
}