use crate::{
error::Error,
protocol::{
caps::{
Capabilities,
address::Transfer,
other::HostCooperation,
set_window::{ColorInterleaving, ScanKind},
},
data::{Position, Truncation, width_code},
window::{Channel, Window, validate_set},
},
};
const COARSE_DIVISOR: u16 = 1200;
const INFRARED_BYTES: Position = Position::INFRARED_FIRST.union(Position::INFRARED_LAST);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Layout {
pub pixels: u32,
pub lines: u32,
pub pitch: u32,
pub line_pitch: u32,
pub dpi: u32,
pub bytes_per_sample: u8,
pub bits_per_sample: u8,
pub channels: Vec<u8>,
pub interleaving: ColorInterleaving,
pub readings_per_line: u8,
pub ccd_lines: u8,
pub packed_rows: u8,
pub registration_gap: u32,
pub granule: usize,
pub truncated_bytes_line: (u32, u32),
pub truncated_bytes_once: (u32, u32),
pub truncated_lines_frame: (u32, u32),
pub multiline_registered: bool,
}
impl Layout {
pub fn single_line(pixels: u32, lines: u32, channels: Vec<u8>) -> Self {
Self {
pixels,
lines,
pitch: 1,
line_pitch: 1,
dpi: 4000,
bytes_per_sample: 2,
bits_per_sample: 16,
channels,
interleaving: ColorInterleaving::LINE_WITHOUT_DISTANCE,
readings_per_line: 1,
ccd_lines: 1,
packed_rows: 1,
registration_gap: 0,
granule: 1,
truncated_bytes_line: (0, 0),
truncated_bytes_once: (0, 0),
truncated_lines_frame: (0, 0),
multiline_registered: false,
}
}
}
fn bad(reason: String) -> Error {
Error::Unsupported {
op: "image layout",
reason,
}
}
fn pitches(caps: &Capabilities, window: &Window) -> Result<(u32, u32), Error> {
let optical = u32::from(caps.address.x_axis.optical_dpi);
let asked = u32::from(window.resolution.0);
if optical == 0 || asked == 0 {
return Err(bad(format!(
"cannot pitch {asked} dpi against an optical resolution of {optical}"
)));
}
let snap = |raw: u32| match window.scanning_kind.contains(ScanKind::THUMBNAIL) {
true => raw.max(1),
false => caps.address.pitch_rule.snap(raw),
};
let optical_y = u32::from(caps.address.y_axis.optical_dpi).max(optical);
let asked_y = match window.resolution.1 {
0 => asked,
y => u32::from(y),
};
Ok((snap(optical / asked), snap(optical_y / asked_y)))
}
fn packed_rows(caps: &Capabilities, interleaving: ColorInterleaving) -> u8 {
let packed = interleaving.contains(ColorInterleaving::MULTILINE_SIMULTANEOUS)
&& !caps
.features
.cooperation
.contains(HostCooperation::MULTI_LINE);
match packed {
true => caps.address.lines.max(1),
false => 1,
}
}
fn read_granule(caps: &Capabilities, layout: &Layout, truncated: Option<&Truncation>) -> usize {
let transfer = caps.address.transfer;
let rows = usize::from(layout.packed_rows);
let whole_line = (layout.bytes_per_line() as usize * rows).max(1);
if transfer.contains(Transfer::READ_LINE_COLS) {
return match layout.even_readings() {
true => (layout.bytes_per_reading(0) as usize * rows).max(1),
false => whole_line,
};
}
if !transfer.contains(Transfer::READ_LINE) {
return 1;
}
let (per_color, all_colors) = truncated.map_or((0, 0), |t| {
(
usize::from(t.per_color.first) + usize::from(t.per_color.last),
usize::from(t.all_colors.first) + usize::from(t.all_colors.last),
)
});
if all_colors > 0 {
return whole_line;
}
(layout.pixels as usize * usize::from(layout.bytes_per_sample) + per_color).max(1)
}
impl Layout {
pub fn new(
caps: &Capabilities,
windows: &[Window],
divisor: u16,
truncated_by_driver: Option<&Truncation>,
) -> Result<Self, Error> {
validate_set(windows)?;
let first = &windows[0];
let optical = u32::from(caps.address.x_axis.optical_dpi);
let (pitch, line_pitch) = pitches(caps, first)?;
let (pixels, lines) = if divisor == COARSE_DIVISOR {
let scale = |v: u32, p: u32| {
(u64::from(v) * u64::from(optical) / (u64::from(COARSE_DIVISOR) * u64::from(p)))
as u32
};
(scale(first.size.0, pitch), scale(first.size.1, line_pitch))
} else {
(first.size.0 / pitch, first.size.1 / line_pitch)
};
let bytes_per_sample = first.bpp.div_ceil(8);
if width_code(bytes_per_sample).is_none() {
return Err(bad(format!(
"{} bits a sample needs {bytes_per_sample} bytes, which 2-11-4 cannot encode",
first.bpp
)));
}
let channels: Vec<u8> = windows.iter().map(|w| w.id).collect();
let mut truncated_bytes_line = (0, 0);
let mut truncated_bytes_once = (0, 0);
let mut truncated_lines_frame = (0, 0);
if let Some(t) = truncated_by_driver {
truncated_bytes_line = (
u32::from(t.per_color.first) + u32::from(t.all_colors.first),
u32::from(t.per_color.last) + u32::from(t.all_colors.last),
);
truncated_bytes_once = match t.position.intersects(INFRARED_BYTES) {
true => (
u32::from(t.per_color.first) + u32::from(t.infrared_reading.first),
u32::from(t.per_color.last) + u32::from(t.infrared_reading.last),
),
false => truncated_bytes_line,
};
truncated_lines_frame = (u32::from(t.lines.first), u32::from(t.lines.last));
}
let mut layout = Self {
pixels,
lines,
pitch,
line_pitch,
dpi: optical / pitch,
bytes_per_sample,
bits_per_sample: first.bpp,
channels,
interleaving: first.color_interleaving,
readings_per_line: first.multiple_reading.saturating_add(1),
ccd_lines: caps.address.lines,
packed_rows: packed_rows(caps, first.color_interleaving),
registration_gap: u32::from(caps.address.line_gap) / line_pitch,
granule: 1,
truncated_bytes_line,
truncated_bytes_once,
truncated_lines_frame,
multiline_registered: false,
};
layout.granule = read_granule(caps, &layout, truncated_by_driver);
Ok(layout)
}
pub fn width_code(&self) -> u8 {
width_code(self.bytes_per_sample).expect("checked when the layout was built")
}
pub fn colors(&self) -> impl Iterator<Item = u8> + '_ {
self.channels
.iter()
.copied()
.filter(|&id| Channel::from(id).is_color())
}
pub fn readings(&self) -> u32 {
u32::from(self.readings_per_line).max(1)
}
pub fn bytes_per_reading(&self, reading: u32) -> u32 {
let colors = self.colors().count() as u32;
let once = self.channels.len() as u32 - colors;
let readouts = colors + if reading == 0 { once } else { 0 };
let (first, last) = self.truncated_bytes(reading);
self.pixels * u32::from(self.bytes_per_sample) * readouts + first + last
}
pub fn truncated_bytes(&self, reading: u32) -> (u32, u32) {
match reading == 0 && !self.even_readings() {
true => self.truncated_bytes_once,
false => self.truncated_bytes_line,
}
}
pub fn even_readings(&self) -> bool {
self.readings() == 1 || self.channels.len() as u32 == self.colors().count() as u32
}
pub fn bytes_per_line(&self) -> u32 {
(0..self.readings())
.map(|r| self.bytes_per_reading(r))
.sum()
}
pub fn total_bytes(&self) -> u64 {
u64::from(self.bytes_per_line())
* u64::from(self.lines + self.truncated_lines_frame.0 + self.truncated_lines_frame.1)
}
pub fn readouts(&self) -> u32 {
let repeated = self
.channels
.iter()
.filter(|id| Channel::from(**id).is_color())
.count() as u32;
let once = self.channels.len() as u32 - repeated;
repeated * u32::from(self.readings_per_line) + once
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::{
caps::set_window::ScanKind,
caps::{
Page,
address::{Address, PitchRule},
identity::Identity,
other::Features,
set_window::SetWindowFunction,
},
data::{Edges, Position},
window::{Composition, LENGTH},
};
fn caps(transfer: u8, line_gap: u8, lines: u8) -> Capabilities {
let mut p = vec![0u8; 91];
p[1] = Address::PAGE_CODE;
p[3] = 87;
p[4] = transfer;
p[16] = 0x42;
p[18..20].copy_from_slice(&4000u16.to_be_bytes());
p[20..22].copy_from_slice(&4000u16.to_be_bytes());
p[22..24].copy_from_slice(&666u16.to_be_bytes());
p[85] = line_gap;
p[86] = lines;
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;
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 window(id: u8, dpi: u16, size: (u32, u32)) -> Window {
let mut w = Window::try_from(&[0u8; LENGTH][..]).unwrap();
w.id = id;
w.resolution = (dpi, dpi);
w.size = size;
w.bpp = 16;
w.color_interleaving = ColorInterleaving::LINE_WITHOUT_DISTANCE;
w.composition = Composition::MultilevelBW;
w
}
fn rgb(dpi: u16, size: (u32, u32)) -> Vec<Window> {
[1, 2, 3]
.iter()
.map(|&id| {
let mut w = window(id, dpi, size);
w.composition = Composition::MultilevelRGB;
w
})
.collect()
}
#[test]
fn only_an_unregistered_multiline_read_packs_its_rows() {
let five = caps(0x02, 0, 2);
assert_eq!(
packed_rows(&five, ColorInterleaving::MULTILINE_SIMULTANEOUS),
2
);
assert_eq!(
packed_rows(&five, ColorInterleaving::LINE_WITHOUT_DISTANCE),
1
);
let mut nine = caps(0x02, 0, 3);
nine.features.cooperation |= HostCooperation::MULTI_LINE;
assert_eq!(
packed_rows(&nine, ColorInterleaving::MULTILINE_SIMULTANEOUS),
1
);
}
#[test]
fn the_first_real_scan_still_measures_80000_bytes() {
let l = Layout::new(
&caps(0x01, 12, 3),
&[window(1, 666, (1200, 1200))],
4000,
None,
)
.unwrap();
assert_eq!(l.pitch, 6);
assert_eq!((l.pixels, l.lines), (200, 200));
assert_eq!(l.total_bytes(), 80000);
}
#[test]
fn the_pitch_ladder_matches_table_2_10_5() {
for (asked, dpi, pitch) in [
(4000, 4000, 1),
(2001, 4000, 1),
(2000, 2000, 2),
(1334, 2000, 2),
(1333, 1333, 3),
(1001, 1333, 3),
(1000, 1000, 4),
(800, 1000, 4),
(667, 1000, 4),
(666, 666, 6),
(334, 666, 6),
(333, 333, 12),
] {
let l =
Layout::new(&caps(0x01, 12, 3), &rgb(asked, (12000, 12000)), 4000, None).unwrap();
assert_eq!((l.pitch, l.dpi), (pitch, dpi), "{asked} dpi");
assert_eq!(l.pixels, 12000 / pitch, "{asked} dpi");
}
}
#[test]
fn a_thumbnail_pitch_is_not_snapped_to_the_image_ladder() {
let mut windows = rgb(83, (8964, 34644));
windows.truncate(1);
windows[0].composition = Composition::MultilevelBW;
windows[0].scanning_kind = ScanKind::THUMBNAIL;
let l = Layout::new(&caps(0x01, 12, 3), &windows, 4000, None).unwrap();
assert_eq!(l.pitch, 48);
assert_eq!((l.pixels, l.lines), (186, 721));
assert_eq!(l.total_bytes(), 268212);
windows[0].scanning_kind = ScanKind::IMAGE;
assert_eq!(
Layout::new(&caps(0x01, 12, 3), &windows, 4000, None)
.unwrap()
.pitch,
12
);
}
#[test]
fn a_half_y_resolution_halves_the_lines() {
let square = Layout::new(&caps(0x01, 12, 3), &rgb(666, (10000, 1200)), 4000, None).unwrap();
assert_eq!((square.pixels, square.lines), (1666, 200));
assert_eq!(square.total_bytes(), 1999200);
let mut half = rgb(666, (10000, 1200));
for w in &mut half {
w.resolution = (666, 333);
}
let half = Layout::new(&caps(0x01, 12, 3), &half, 4000, None).unwrap();
assert_eq!((half.pixels, half.lines), (1666, 100));
assert_eq!(half.total_bytes(), 999600);
assert_eq!((half.pitch, half.line_pitch), (6, 12));
}
#[test]
fn the_one_plus_even_rule_drops_odd_pitches() {
assert_eq!(PitchRule::OnePlusEven.snap(1), 1);
assert_eq!(PitchRule::OnePlusEven.snap(2), 2);
assert_eq!(PitchRule::OnePlusEven.snap(3), 2);
assert_eq!(PitchRule::OnePlusEven.snap(7), 6);
assert_eq!(PitchRule::Continuous.snap(7), 7);
}
#[test]
fn the_coarse_divisor_scales_the_window_to_pixels() {
let windows = rgb(4000, (1200, 2400));
let fine = Layout::new(&caps(0x01, 12, 3), &windows, 4000, None).unwrap();
let coarse = Layout::new(&caps(0x01, 12, 3), &windows, 1200, None).unwrap();
assert_eq!((fine.pixels, fine.lines), (1200, 2400));
assert_eq!((coarse.pixels, coarse.lines), (4000, 8000));
}
#[test]
fn the_read_granule_follows_the_advertised_units() {
let windows = rgb(4000, (10000, 13860));
let line = 10000 * 2;
let granule = |transfer| {
Layout::new(&caps(transfer, 1, 2), &windows, 4000, None)
.unwrap()
.granule
};
assert_eq!(granule(0x01), 1);
assert_eq!(granule(0x03), line * 3);
assert_eq!(granule(0x05), line);
}
#[test]
fn the_read_granule_counts_the_bytes_the_unit_attaches_to_a_line() {
let truncation = Truncation {
position: Position::ALL_LAST,
all_colors: Edges {
first: 0,
last: 448,
},
..Default::default()
};
let mut windows = rgb(83, (4608, 292992));
for w in &mut windows {
w.scanning_kind = ScanKind::THUMBNAIL;
}
let l = Layout::new(&caps(0x03, 12, 3), &windows, 4000, Some(&truncation)).unwrap();
assert_eq!(l.pixels, 96);
assert_eq!(l.bytes_per_line(), 1024);
assert_eq!(l.granule, 1024);
assert_eq!(128 * 1024 / l.granule * l.granule, 131072);
}
#[test]
fn a_single_line_granule_gives_way_to_the_whole_line() {
let windows = rgb(4000, (10000, 13860));
let granule = |t: &Truncation| {
Layout::new(&caps(0x05, 1, 2), &windows, 4000, Some(t))
.unwrap()
.granule
};
let per_color = Truncation {
position: Position::COLOR_LAST,
per_color: Edges { first: 0, last: 16 },
..Default::default()
};
assert_eq!(granule(&per_color), 10000 * 2 + 16);
let all_colors = Truncation {
position: Position::ALL_LAST,
all_colors: Edges { first: 0, last: 16 },
..Default::default()
};
assert_eq!(granule(&all_colors), 10000 * 2 * 3 + 16);
}
#[test]
fn every_reading_of_a_line_carries_what_the_unit_attaches() {
let truncation = Truncation {
position: Position::ALL_LAST,
all_colors: Edges {
first: 0,
last: 394,
},
..Default::default()
};
let mut windows = rgb(4000, (3945, 5658));
for w in &mut windows {
w.multiple_reading = 1;
}
let l = Layout::new(&caps(0x03, 1, 1), &windows, 4000, Some(&truncation)).unwrap();
assert_eq!((l.pixels, l.lines, l.readings()), (3945, 5658, 2));
assert_eq!(l.bytes_per_reading(0), 24064);
assert_eq!(l.bytes_per_line(), 48128);
assert_eq!(l.total_bytes(), 272_308_224);
assert_eq!(l.granule, 24064);
assert_eq!(128 * 1024 / l.granule * l.granule, 120_320);
}
#[test]
fn a_channel_read_once_rides_with_the_first_reading() {
let truncation = Truncation {
position: Position::ALL_LAST | Position::INFRARED_LAST,
all_colors: Edges {
first: 0,
last: 394,
},
infrared_reading: Edges {
first: 0,
last: 184,
},
..Default::default()
};
let mut windows = rgb(4000, (3945, 5670));
windows.push(window(9, 4000, (3945, 5670)));
for w in &mut windows {
w.multiple_reading = 1;
w.composition = Composition::MultilevelRGB;
}
let l = Layout::new(&caps(0x03, 1, 1), &windows, 4000, Some(&truncation)).unwrap();
assert!(!l.even_readings());
assert_eq!(l.bytes_per_reading(0), 31744);
assert_eq!(l.bytes_per_reading(1), 24064);
assert_eq!(l.bytes_per_line(), 55808);
assert_eq!(l.total_bytes(), 316_431_360);
assert_eq!(l.granule, l.bytes_per_line() as usize);
}
#[test]
fn a_reading_of_colors_alone_takes_the_count_of_all_colors() {
let truncation = Truncation {
position: Position::ALL_LAST,
all_colors: Edges {
first: 0,
last: 394,
},
..Default::default()
};
let mut windows = rgb(4000, (3945, 5670));
windows.push(window(9, 4000, (3945, 5670)));
for w in &mut windows {
w.multiple_reading = 1;
w.composition = Composition::MultilevelRGB;
}
let l = Layout::new(&caps(0x03, 1, 1), &windows, 4000, Some(&truncation)).unwrap();
assert_eq!(l.bytes_per_reading(0), 3945 * 2 * 4 + 394);
assert_eq!(l.bytes_per_reading(1), 3945 * 2 * 3 + 394);
}
#[test]
fn one_reading_of_every_channel_takes_the_count_of_all_colors() {
let truncation = Truncation {
position: Position::ALL_LAST,
all_colors: Edges {
first: 0,
last: 312,
},
..Default::default()
};
let mut windows = rgb(4000, (281, 5669));
windows.push(window(9, 4000, (281, 5669)));
for w in &mut windows {
w.composition = Composition::MultilevelRGB;
}
let l = Layout::new(&caps(0x03, 1, 1), &windows, 4000, Some(&truncation)).unwrap();
assert_eq!(l.bytes_per_reading(0), 2560);
assert_eq!(l.bytes_per_line(), 2560);
}
#[test]
fn multiple_reading_multiplies_the_byte_count() {
let mut windows = rgb(4000, (10000, 13860));
let single = Layout::new(&caps(0x01, 12, 3), &windows, 4000, None).unwrap();
assert_eq!(single.readings_per_line, 1);
assert_eq!(single.total_bytes(), 10000 * 2 * 3 * 13860);
for w in &mut windows {
w.multiple_reading = 15;
}
let sixteen = Layout::new(&caps(0x01, 12, 3), &windows, 4000, None).unwrap();
assert_eq!(sixteen.readings_per_line, 16);
assert_eq!(sixteen.total_bytes(), single.total_bytes() * 16);
}
#[test]
fn the_registration_gap_shrinks_with_the_pitch() {
let gap = |dpi| {
Layout::new(&caps(0x01, 12, 3), &rgb(dpi, (10000, 13860)), 4000, None)
.unwrap()
.registration_gap
};
assert_eq!(gap(4000), 12);
assert_eq!(gap(2000), 6);
assert_eq!(gap(1333), 4);
let mut preview = rgb(666, (10000, 13860));
for w in &mut preview {
w.resolution = (666, 333);
}
let preview = Layout::new(&caps(0x01, 12, 3), &preview, 4000, None).unwrap();
assert_eq!((preview.pitch, preview.line_pitch), (6, 12));
assert_eq!(preview.registration_gap, 1);
}
#[test]
fn a_window_set_that_disagrees_has_no_layout() {
let mut windows = rgb(4000, (10000, 13860));
windows[2].size.0 = 9000;
assert!(Layout::new(&caps(0x01, 12, 3), &windows, 4000, None).is_err(),);
let mut windows = rgb(4000, (10000, 13860));
windows[2].exposure = 71125;
assert!(Layout::new(&caps(0x01, 12, 3), &windows, 4000, None).is_ok(),);
}
#[test]
fn an_empty_window_set_has_no_layout() {
assert!(Layout::new(&caps(0x01, 12, 3), &[], 4000, None).is_err());
}
}
#[cfg(test)]
mod readouts {
use super::*;
fn prescan(readings: u8) -> Layout {
Layout {
pixels: 1494,
lines: 1098,
pitch: 6,
line_pitch: 12,
dpi: 666,
bytes_per_sample: 2,
bits_per_sample: 16,
channels: vec![9, 1, 2, 3],
interleaving: ColorInterleaving::MULTILINE_SIMULTANEOUS,
readings_per_line: readings,
ccd_lines: 3,
packed_rows: 1,
registration_gap: 1,
granule: 1,
truncated_bytes_line: (0, 0),
truncated_bytes_once: (0, 0),
truncated_lines_frame: (0, 0),
multiline_registered: false,
}
}
#[test]
fn a_pass_is_as_long_as_the_captures_measured() {
assert_eq!(prescan(1).total_bytes(), 13_123_296);
assert_eq!(prescan(16).total_bytes(), 160_760_376);
let short = Layout {
lines: 558,
..prescan(1)
};
assert_eq!(short.total_bytes(), 6_669_216);
}
#[test]
fn only_the_color_channels_repeat() {
assert_eq!(prescan(1).readouts(), 4);
assert_eq!(prescan(16).readouts(), 49);
assert_eq!(
Layout {
channels: vec![1, 2, 3],
..prescan(16)
}
.readouts(),
48
);
}
}