use crate::{
error::Error,
protocol::{
caps::{
Capabilities,
address::Axis,
set_window::{ColorComponents, ColorInterleaving, ScanKind, ScanMode},
},
data::Rect,
window::{Channel, Composition, Flags, LENGTH, Window, deepest_depth},
},
scan::framing,
};
use tracing::*;
pub const MAX_SAMPLES: u8 = 16;
pub(crate) fn block(caps: &Capabilities) -> u32 {
u32::from(caps.address.line_gap) * u32::from(caps.address.lines)
}
pub(crate) fn whole_blocks(caps: &Capabilities, extent: u32) -> u32 {
match block(caps) {
0 | 1 => extent,
block => extent.div_ceil(block) * block,
}
}
pub(crate) fn reachable_blocks(caps: &Capabilities, extent: u32) -> u32 {
let limit = caps.address.y_axis.boundary;
let grown = whole_blocks(caps, extent);
if grown <= limit {
return grown;
}
let kept = match block(caps) {
0 | 1 => limit,
block => limit / block * block,
};
debug!(
wanted = grown,
limit, kept, "the format is longer than the axis reaches, so the frame is trimmed"
);
kept
}
pub(crate) fn color_channels(caps: &Capabilities) -> Vec<Channel> {
match caps.set_window.components.contains(ColorComponents::RGB) {
true => vec![Channel::Red, Channel::Green, Channel::Blue],
false => vec![Channel::Default],
}
}
pub(crate) fn blank(caps: &Capabilities, channels: &[Channel]) -> Result<Vec<Window>, Error> {
let bpp = deepest_depth(caps.set_window.depth).ok_or_else(|| Error::Unsupported {
op: "scan window",
reason: "this unit advertises no pixel depth".into(),
})?;
let composition = match channels.iter().filter(|c| c.is_color()).count() {
1 => Composition::MultilevelBW,
_ => Composition::MultilevelRGB,
};
Ok(channels
.iter()
.map(|channel| {
let mut w =
Window::try_from(&[0u8; LENGTH][..]).expect("a zeroed descriptor is long enough");
w.id = channel.id();
w.composition = composition;
w.bpp = bpp;
w.ae_value = 255;
w
})
.collect())
}
#[derive(Debug, Clone, Copy)]
pub struct Recipe {
pub dpi: u16,
pub samples: u8,
pub interleaving: ColorInterleaving,
pub infrared: bool,
}
impl Recipe {
pub fn metering(&self, caps: &Capabilities) -> Self {
let offered = caps.set_window.interleaving;
Self {
dpi: caps.address.x_axis.dpi_range.start,
samples: 1,
interleaving: match offered.contains(ColorInterleaving::LINE_WITHOUT_DISTANCE) {
true => ColorInterleaving::LINE_WITHOUT_DISTANCE,
false => ColorInterleaving::MULTILINE_SIMULTANEOUS,
},
infrared: self.infrared,
}
}
pub fn supported(&self, caps: &Capabilities) -> Result<(), Error> {
let ladder = caps.address.x_axis.dpi_range;
if !ladder.contains(&self.dpi) {
return Err(Error::Unsupported {
op: "scan resolution",
reason: format!(
"{} dpi is outside the {} to {} this unit scans",
self.dpi, ladder.start, ladder.last
),
});
}
if !(1..=MAX_SAMPLES).contains(&self.samples) {
return Err(Error::Unsupported {
op: "readings a line",
reason: format!(
"{} readings of a line is outside 1 to {MAX_SAMPLES}",
self.samples
),
});
}
let offered = caps.set_window.interleaving;
match self.interleaving.bits().count_ones() == 1 && offered.contains(self.interleaving) {
true => Ok(()),
false => Err(Error::Unsupported {
op: "color interleaving",
reason: format!(
"this unit does not read the CCD {}, only {offered:?}",
Self::reading(self.interleaving)
),
}),
}
}
fn reading(interleaving: ColorInterleaving) -> &'static str {
match interleaving {
ColorInterleaving::LINE_WITHOUT_DISTANCE => "one row at a time",
ColorInterleaving::MULTILINE_SIMULTANEOUS => "three rows at once",
_ => "that way",
}
}
fn blocks(&self, caps: &Capabilities, top: u32, extent: u32) -> u32 {
if !self
.interleaving
.contains(ColorInterleaving::MULTILINE_SIMULTANEOUS)
{
return extent;
}
let y = &caps.address.y_axis;
let room = y.address_range.last.saturating_sub(top).min(y.boundary);
let grown = whole_blocks(caps, extent);
match grown <= room {
true => grown,
false => grown.saturating_sub(block(caps)),
}
}
pub fn windows(&self, caps: &Capabilities, frame: Rect) -> Result<Vec<Window>, Error> {
self.supported(caps)?;
let mut channels = color_channels(caps);
if self.infrared {
channels.insert(0, Channel::Infrared);
}
let (x, y) = (&caps.address.x_axis, &caps.address.y_axis);
let clamp =
|v: u32, axis: &Axis| v.clamp(axis.address_range.start, axis.address_range.last);
let origin = (clamp(frame.left, x), clamp(frame.top, y));
let extent = self.blocks(caps, origin.1, frame.bottom.saturating_sub(origin.1));
framing::reachable(caps, extent)?;
let size = (frame.right.saturating_sub(origin.0).min(x.boundary), extent);
let native = self
.interleaving
.contains(ColorInterleaving::MULTILINE_SIMULTANEOUS)
&& self.dpi < caps.address.x_axis.optical_dpi;
let fast = caps.set_window.mode.contains(ScanMode::HIGH_SPEED);
let mut mode = match native && fast {
true => ScanMode::HIGH_SPEED,
false => ScanMode::NORMAL_QUALITY,
};
if self.samples > 1 {
mode |= ScanMode::MULTI_READING;
}
let mut flags = Flags::POSITIVE;
if !native {
flags |= Flags::AVERAGING;
}
let mut windows = blank(caps, &channels)?;
for w in &mut windows {
w.resolution = (self.dpi, self.dpi);
w.origin = origin;
w.size = size;
w.scanning_kind = ScanKind::IMAGE;
w.scanning_mode = mode;
w.color_interleaving = self.interleaving;
w.flags = flags;
w.multiple_reading = self.samples - 1;
}
Ok(windows)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::protocol::caps::{
Page, address::Address, identity::Identity, other::Features, set_window::SetWindowFunction,
};
pub(crate) fn caps() -> Capabilities {
let mut p = vec![0u8; 91];
p[1] = Address::PAGE_CODE;
p[3] = 87;
for axis in [18, 40] {
p[axis..axis + 2].copy_from_slice(&4000u16.to_be_bytes());
p[axis + 2..axis + 4].copy_from_slice(&4000u16.to_be_bytes());
p[axis + 4..axis + 6].copy_from_slice(&666u16.to_be_bytes());
}
p[24..28].copy_from_slice(&20000u32.to_be_bytes());
p[85] = 12;
p[86] = 3;
p[36..40].copy_from_slice(&10000u32.to_be_bytes());
p[46..50].copy_from_slice(&20000u32.to_be_bytes());
p[58..62].copy_from_slice(&20000u32.to_be_bytes());
let address = Address::try_from(&Page::new(Address::PAGE_CODE, p).unwrap()).unwrap();
let mut d = vec![0u8; 28];
d[1] = SetWindowFunction::PAGE_CODE;
d[3] = 24;
d[5] = ScanMode::HIGH_SPEED.bits();
d[6] = (ColorInterleaving::MULTILINE_SIMULTANEOUS
| ColorInterleaving::LINE_WITHOUT_DISTANCE)
.bits();
d[7] = ColorComponents::RGB.bits();
d[10] = crate::protocol::caps::set_window::BitDepth::BIT_16.bits();
let set_window =
SetWindowFunction::try_from(&Page::new(SetWindowFunction::PAGE_CODE, d).unwrap())
.unwrap();
let mut e = vec![0u8; 39];
e[1] = Features::PAGE_CODE;
e[3] = 35;
let features = Features::try_from(&Page::new(Features::PAGE_CODE, e).unwrap()).unwrap();
let mut i = vec![0u8; 36];
i[4] = 31;
Capabilities {
identity: Identity::parse(&i).unwrap(),
address,
features,
set_window,
ccd: None,
frames: None,
}
}
fn frame() -> Rect {
Rect {
top: 2236,
left: 518,
bottom: 2236 + 8964,
right: 518 + 8964,
}
}
fn recipe() -> Recipe {
Recipe {
dpi: 4000,
samples: 1,
interleaving: ColorInterleaving::MULTILINE_SIMULTANEOUS,
infrared: false,
}
}
#[test]
fn infrared_joins_the_set_without_joining_the_planes() {
let windows = Recipe {
infrared: true,
..recipe()
}
.windows(&caps(), frame())
.expect("windows");
assert_eq!(
windows.iter().map(|w| w.id).collect::<Vec<_>>(),
vec![9, 1, 2, 3]
);
assert!(
windows
.iter()
.all(|w| w.composition == Composition::MultilevelRGB)
);
}
#[test]
fn metering_carries_the_channels_the_scan_does() {
let caps = caps();
let ids = |recipe: Recipe| {
recipe
.metering(&caps)
.windows(&caps, frame())
.expect("windows")
.iter()
.map(|w| w.id)
.collect::<Vec<_>>()
};
assert_eq!(ids(recipe()), vec![1, 2, 3]);
assert_eq!(
ids(Recipe {
infrared: true,
..recipe()
}),
vec![9, 1, 2, 3]
);
}
#[test]
fn multisampling_is_stated_in_both_bytes() {
let four = Recipe {
samples: 4,
..recipe()
}
.windows(&caps(), frame())
.expect("windows");
assert_eq!(four[0].multiple_reading, 3);
assert!(four[0].scanning_mode.contains(ScanMode::MULTI_READING));
let one = recipe().windows(&caps(), frame()).expect("windows");
assert_eq!(one[0].multiple_reading, 0);
assert!(!one[0].scanning_mode.contains(ScanMode::MULTI_READING));
}
#[test]
fn a_resolution_off_the_unit_is_refused_before_anything_moves() {
let caps = caps();
let ladder = caps.address.x_axis.dpi_range;
for dpi in [ladder.start, 2000, ladder.last] {
assert!(Recipe { dpi, ..recipe() }.supported(&caps).is_ok(), "{dpi}");
}
for dpi in [0, ladder.start - 1, ladder.last + 1] {
assert!(
Recipe { dpi, ..recipe() }.supported(&caps).is_err(),
"{dpi}"
);
}
}
#[test]
fn a_reading_count_past_the_nibble_is_refused() {
let caps = caps();
assert!(
Recipe {
samples: 0,
..recipe()
}
.supported(&caps)
.is_err()
);
assert!(
Recipe {
samples: 17,
..recipe()
}
.supported(&caps)
.is_err()
);
assert!(
Recipe {
samples: 16,
..recipe()
}
.supported(&caps)
.is_ok()
);
}
#[test]
fn an_interleaving_this_unit_has_not_got_is_refused() {
let mut caps = caps();
caps.set_window.interleaving = ColorInterleaving::LINE_WITHOUT_DISTANCE;
assert!(recipe().supported(&caps).is_err());
assert!(recipe().windows(&caps, frame()).is_err());
}
#[test]
fn a_format_longer_than_the_axis_is_trimmed_to_it() {
use crate::protocol::caps::film::FilmFormat;
let mut caps = caps();
caps.address.y_axis.boundary = 13176;
caps.address.line_gap = 8;
caps.address.lines = 3;
let six_by_nine = FilmFormat::F69.height_dots(caps.address.y_axis.optical_dpi);
assert_eq!(six_by_nine, 13228);
assert!(
whole_blocks(&caps, six_by_nine) > caps.address.y_axis.boundary,
"the format has to overrun the axis for this to be the case under test"
);
let kept = reachable_blocks(&caps, six_by_nine);
assert_eq!(kept, 13176);
assert_eq!(kept % block(&caps), 0, "still whole blocks");
framing::reachable(&caps, kept).expect("the stage can step to a trimmed frame");
}
#[test]
fn a_multi_line_window_is_whole_blocks_of_the_readout() {
let frame = Rect {
top: 2236,
left: 518,
bottom: 2236 + 8819,
right: 518 + 8964,
};
let windows = recipe().windows(&caps(), frame).expect("windows");
assert_eq!(windows[0].size.1, 8820);
assert_eq!(windows[0].size.1 % 36, 0);
let single = Recipe {
interleaving: ColorInterleaving::LINE_WITHOUT_DISTANCE,
..recipe()
}
.windows(&caps(), frame)
.expect("windows");
assert_eq!(single[0].size.1, 8819);
}
#[test]
fn a_frame_against_the_end_of_the_axis_shrinks_instead() {
let caps = caps();
let end = caps.address.y_axis.address_range.last;
let frame = Rect {
top: end - 8819,
left: 518,
bottom: end,
right: 518 + 8964,
};
let windows = recipe().windows(&caps, frame).expect("windows");
assert_eq!(windows[0].size.1, 8784);
assert!(windows[0].origin.1 + windows[0].size.1 <= end);
}
#[test]
fn a_reduced_multi_line_pass_is_shaped_like_a_preview() {
let caps = caps();
let optical = caps.address.x_axis.optical_dpi;
let full = recipe().windows(&caps, frame()).expect("windows");
assert!(full[0].flags.contains(Flags::AVERAGING));
assert_eq!(full[0].scanning_mode, ScanMode::NORMAL_QUALITY);
let reduced = Recipe {
dpi: optical / 6,
..recipe()
}
.windows(&caps, frame())
.expect("windows");
assert!(!reduced[0].flags.contains(Flags::AVERAGING));
assert!(reduced[0].scanning_mode.contains(ScanMode::HIGH_SPEED));
let single = Recipe {
dpi: optical / 6,
interleaving: ColorInterleaving::LINE_WITHOUT_DISTANCE,
..recipe()
}
.windows(&caps, frame())
.expect("windows");
assert!(single[0].flags.contains(Flags::AVERAGING));
assert_eq!(single[0].scanning_mode, ScanMode::NORMAL_QUALITY);
}
#[test]
fn the_bytes_pair_the_way_the_hardware_wants() {
let caps = caps();
let optical = caps.address.x_axis.optical_dpi;
let bytes = |dpi, samples| {
let w = Recipe {
dpi,
samples,
..recipe()
}
.windows(&caps, frame())
.expect("windows");
let d = w[0].to_bytes();
(d[40], d[41], d[42], d[43], d[44], d[45])
};
assert_eq!(bytes(optical, 1), (0x00, 0x81, 0x01, 0x02, 0x40, 0xFF));
assert_eq!(bytes(optical, 4), (0x30, 0x81, 0x01, 0x12, 0x40, 0xFF));
assert_eq!(bytes(optical / 6, 1), (0x00, 0x01, 0x01, 0x04, 0x40, 0xFF));
assert_eq!(bytes(optical / 6, 2), (0x10, 0x01, 0x01, 0x14, 0x40, 0xFF));
}
#[test]
fn the_window_is_the_frame() {
let windows = recipe().windows(&caps(), frame()).expect("windows");
assert_eq!(windows[0].origin, (518, 2236));
assert_eq!(windows[0].size, (8964, 8964));
}
}