use super::{
pass::{Pass, Progress},
thumbnail,
};
use crate::{
error::Error,
protocol::{
caps::{Capabilities, address::CoordinateBase, film::FilmFormat, other::DataTypes},
data::{Boundary, FramePosition, FrameTable, Op, PerforationInformation, Rect},
decode::Samples,
},
session::Session,
};
use std::ops::ControlFlow;
use tracing::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Framing {
Published,
Thumbnail,
Perforation,
Address,
}
impl Framing {
pub fn choose(caps: &Capabilities) -> Self {
let rects = caps
.address
.coordinate_base
.contains(CoordinateBase::FRAME_RECTS);
if rects {
if caps.frames.as_ref().is_some_and(|f| f.measured()) {
return Self::Published;
}
if thumbnail::available(caps) {
return Self::Thumbnail;
}
return Self::Address;
}
if caps
.features
.data_types
.contains(DataTypes::PERFORATION_READ)
{
return Self::Perforation;
}
Self::Address
}
}
pub(crate) fn reachable(caps: &Capabilities, extent: u32) -> Result<(), Error> {
if extent == 0 {
return Err(Error::Unsupported {
op: "frame table",
reason: "a frame of length 0 is not a frame".into(),
});
}
let limit = caps.address.y_axis.boundary;
match extent > limit {
true => Err(Error::Unsupported {
op: "frame table",
reason: format!(
"a frame of {extent} is past the {limit} boundary and would stall the stage"
),
}),
false => Ok(()),
}
}
pub fn table(caps: &Capabilities) -> Result<Boundary, Error> {
let Some(published) = caps.frames.as_ref() else {
return Ok(Boundary::default());
};
let mut frames = Vec::new();
for image in &published.images {
let Some(extent) = image.length else {
continue;
};
let extent = super::window::whole_blocks(caps, extent);
reachable(caps, extent)?;
frames.push(Rect {
top: image.top,
left: image.left,
bottom: image.top + extent,
right: image.left + image.width,
});
}
Ok(Boundary { frames })
}
pub fn frames_on_medium(caps: &Capabilities) -> u32 {
let y = &caps.address.y_axis;
let span = y.address_range.last.saturating_sub(y.address_range.start) + 1;
match y.boundary {
0 => 1,
boundary => (span / boundary).max(1),
}
}
pub fn self_feeding(caps: &Capabilities) -> bool {
caps.features.execute.supports(Op::Load) && frames_on_medium(caps) == 1
}
pub fn frames(caps: &Capabilities) -> Result<Boundary, Error> {
let (x, y) = (&caps.address.x_axis, &caps.address.y_axis);
let extent = super::window::reachable_blocks(caps, y.boundary);
reachable(caps, extent)?;
let count = frames_on_medium(caps);
debug!(
count,
pitch = y.boundary,
extent,
"framed from the address page"
);
let frames = (0..count)
.map(|n| {
let top = y.address_range.start + n * y.boundary;
Rect {
top,
left: x.address_range.start,
bottom: top + extent,
right: x.address_range.start + x.boundary,
}
})
.collect();
Ok(Boundary { frames })
}
pub fn register(session: &mut Session, frame: Rect) -> Result<bool, Error> {
if Framing::choose(session.capabilities()) != Framing::Perforation {
return Ok(false);
}
let mut table = match session.frames_type2() {
Some(table) => table.clone(),
None => match session.boundaries_type2() {
Ok(table) => table,
Err(e) => {
debug!(%e, "no frame table to register against");
return Ok(false);
}
},
};
if table.frames.is_empty() || table.frames.iter().any(|f| f.top == frame.top) {
return Ok(false);
}
let (line, perf) = record(session, frame.top)?;
table.register(FramePosition::new(frame.top, &perf));
debug!(
top = frame.top,
line,
?perf,
"registered a frame off the table"
);
session.set_boundaries_type2_for_pass(&table)?;
Ok(true)
}
fn record(session: &mut Session, top: u32) -> Result<(usize, PerforationInformation), Error> {
let perfs = session.read_perforations()?;
let caps = session.capabilities();
let line = thumbnail::LinePitch::measured(caps, &perfs)
.unwrap_or_else(|| thumbnail::LinePitch::computed(caps))
.line_at(caps, top);
let perf = perfs.at(line).ok_or_else(|| Error::Unsupported {
op: "frame registration",
reason: format!(
"a frame at {top} needs the perforation reading of thumbnail line {line}, and the last pass measured {}",
perfs.perfs.len()
),
})?;
Ok((line, perf.clone()))
}
pub struct Discovery {
pub table: FrameTable,
pub frames: Vec<Rect>,
pub thumbnail: Option<Pass>,
pub line_pitch: Option<thumbnail::LinePitch>,
}
pub fn discover(
session: &mut Session,
format: Option<FilmFormat>,
samples: &mut Samples,
) -> Result<Discovery, Error> {
discover_with(session, format, samples, |_| ControlFlow::Continue(()))
}
pub fn discover_with(
session: &mut Session,
format: Option<FilmFormat>,
samples: &mut Samples,
on: impl FnMut(Progress) -> ControlFlow<()>,
) -> Result<Discovery, Error> {
let need_format = |session: &Session| FilmFormat::resolve(format, session.capabilities());
let mechanism = Framing::choose(session.capabilities());
debug!(?mechanism, "frame discovery");
match mechanism {
Framing::Published => {
let boundary = table(session.capabilities())?;
let found = boundary.frames.clone();
info!(frames = found.len(), "published frames");
Ok(Discovery {
table: FrameTable::Boundary(boundary),
frames: found,
thumbnail: None,
line_pitch: None,
})
}
Framing::Thumbnail => {
let format = need_format(session)?;
let pass = session.scan_thumbnail_with(samples, on)?;
debug!(
rows = pass.rows,
cols = pass.cols,
complete = pass.complete,
"thumbnail"
);
let optical_dpi = session.capabilities().address.y_axis.optical_dpi;
let length = format.height_dots(optical_dpi);
info!(?format, length, "frame length");
let (measured, pitch) =
thumbnail::frames(session.capabilities(), &pass, samples, length)?;
if !measured.frames.is_empty() {
session.set_boundaries(&measured)?;
}
let found = measured.frames.clone();
info!(frames = found.len(), "detected frames");
Ok(Discovery {
table: FrameTable::Boundary(measured),
frames: found,
thumbnail: Some(pass),
line_pitch: Some(pitch),
})
}
Framing::Address => {
let boundary = frames(session.capabilities())?;
let found = boundary.frames.clone();
Ok(Discovery {
table: FrameTable::Boundary(boundary),
frames: found,
thumbnail: None,
line_pitch: None,
})
}
Framing::Perforation => {
let format = need_format(session)?;
let _ = session.read_perforations()?;
let _ = session.read_boundaries_type2();
let pass = session.scan_thumbnail_with(samples, on)?;
debug!(
rows = pass.rows,
cols = pass.cols,
complete = pass.complete,
"thumbnail"
);
let optical_dpi = session.capabilities().address.y_axis.optical_dpi;
let length = format.height_dots(optical_dpi);
info!(?format, length, "frame length");
let perfs = session.read_perforations()?;
let (measured, length, line_pitch) =
thumbnail::frames_type2(session.capabilities(), &pass, samples, &perfs, length)?;
if !measured.frames.is_empty() {
session.set_boundaries_type2(&measured)?;
}
let x_start = session.capabilities().address.x_axis.address_range.start;
let x_boundary = session.capabilities().address.x_axis.boundary;
let found = measured
.frames
.iter()
.map(|f| f.rect(x_start, x_boundary, length))
.collect::<Vec<_>>();
info!(frames = found.len(), "detected frames");
Ok(Discovery {
table: FrameTable::BoundaryType2(measured),
frames: found,
thumbnail: Some(pass),
line_pitch: Some(line_pitch),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scan::window::tests::caps;
#[test]
fn a_medium_longer_than_the_gate_is_framed_along_its_range() {
let mut caps = caps();
assert_eq!(frames(&caps).expect("frames").frames.len(), 1);
caps.address.y_axis.address_range = (0..=111324).into();
caps.address.y_axis.boundary = 4453;
caps.address.line_gap = 0;
let found = frames(&caps).expect("frames").frames;
assert_eq!(found.len(), 25);
assert_eq!(found[0].bottom - found[0].top, 4453);
assert_eq!(found[24].top, 24 * 4453);
}
}