use super::{BindingStyle, BlankPolicy};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Slot {
pub page: Option<usize>,
pub column: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Sheet {
pub signature: usize,
pub sheet_in_sig: usize,
pub front: Vec<Slot>,
pub back: Vec<Slot>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Layout {
pub style: BindingStyle,
pub source_pages: usize,
pub padded_pages: usize,
pub columns_per_side: usize,
pub sheets_per_signature: usize,
pub signatures: usize,
pub sheets: Vec<Sheet>,
}
pub fn plan(
style: BindingStyle,
sheets_per_signature: usize,
source_pages: usize,
blank: BlankPolicy,
) -> Layout {
let source = source_pages.max(1);
match style {
BindingStyle::SaddleStitch => {
let s = source.div_ceil(4).max(1);
build_folded(style, 1, s, source, blank)
}
BindingStyle::PerfectBound => {
let s = sheets_per_signature.max(1);
let per_sig = 4 * s;
let n = source.div_ceil(per_sig).max(1);
build_folded(style, n, s, source, blank)
}
BindingStyle::Stab | BindingStyle::Concertina => build_sequential(style, source, blank),
}
}
fn folded_positions(s: usize, i: usize) -> (usize, usize, usize, usize) {
let fs = 4 * s;
(fs - 2 * i + 2, 2 * i - 1, 2 * i, fs - 2 * i + 1)
}
fn build_folded(
style: BindingStyle,
n_sigs: usize,
s: usize,
source: usize,
blank: BlankPolicy,
) -> Layout {
let per_sig = 4 * s;
let padded = n_sigs * per_sig;
let mut sheets = Vec::with_capacity(n_sigs * s);
for g in 0..n_sigs {
let off = g * per_sig;
for i in 1..=s {
let (fl, fr, bl, br) = folded_positions(s, i);
sheets.push(Sheet {
signature: g,
sheet_in_sig: i,
front: vec![
slot(off + fl, source, padded, blank, 0),
slot(off + fr, source, padded, blank, 1),
],
back: vec![
slot(off + bl, source, padded, blank, 0),
slot(off + br, source, padded, blank, 1),
],
});
}
}
Layout {
style,
source_pages: source,
padded_pages: padded,
columns_per_side: 2,
sheets_per_signature: s,
signatures: n_sigs,
sheets,
}
}
fn build_sequential(style: BindingStyle, source: usize, blank: BlankPolicy) -> Layout {
let leaves = source.div_ceil(2).max(1);
let padded = 2 * leaves;
let mut sheets = Vec::with_capacity(leaves);
for k in 0..leaves {
sheets.push(Sheet {
signature: 0,
sheet_in_sig: k + 1,
front: vec![slot(2 * k + 1, source, padded, blank, 0)],
back: vec![slot(2 * k + 2, source, padded, blank, 0)],
});
}
Layout {
style,
source_pages: source,
padded_pages: padded,
columns_per_side: 1,
sheets_per_signature: 1,
signatures: 1,
sheets,
}
}
fn slot(pos: usize, source: usize, padded: usize, blank: BlankPolicy, column: usize) -> Slot {
Slot {
page: map_page(pos, source, padded, blank),
column,
}
}
fn map_page(pos: usize, source: usize, padded: usize, blank: BlankPolicy) -> Option<usize> {
if pos < 1 || pos > padded {
return None;
}
let blanks = padded - source;
match blank {
BlankPolicy::Append | BlankPolicy::Error => (pos <= source).then_some(pos),
BlankPolicy::Prepend => (pos > blanks).then(|| pos - blanks),
BlankPolicy::Balance => {
let front = blanks / 2;
if pos <= front || pos - front > source {
None
} else {
Some(pos - front)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
fn assert_complete(layout: &Layout) {
let mut seen: Vec<usize> = Vec::new();
let mut blanks = 0;
for sh in &layout.sheets {
for slot in sh.front.iter().chain(sh.back.iter()) {
match slot.page {
Some(p) => seen.push(p),
None => blanks += 1,
}
}
}
let set: BTreeSet<usize> = seen.iter().copied().collect();
assert_eq!(set.len(), seen.len(), "a source page was imposed twice");
let expected: BTreeSet<usize> = (1..=layout.source_pages).collect();
assert_eq!(set, expected, "missing/extra page");
assert_eq!(
blanks,
layout.padded_pages - layout.source_pages,
"blank count mismatch"
);
}
fn pages(side: &[Slot]) -> Vec<Option<usize>> {
side.iter().map(|s| s.page).collect()
}
#[test]
fn saddle_four_pages_is_classic_imposition() {
let l = plan(BindingStyle::SaddleStitch, 1, 4, BlankPolicy::Append);
assert_eq!(l.sheets.len(), 1);
assert_eq!(l.padded_pages, 4);
assert_eq!(pages(&l.sheets[0].front), vec![Some(4), Some(1)]);
assert_eq!(pages(&l.sheets[0].back), vec![Some(2), Some(3)]);
assert_complete(&l);
}
#[test]
fn saddle_eight_pages_two_nested_sheets() {
let l = plan(BindingStyle::SaddleStitch, 1, 8, BlankPolicy::Append);
assert_eq!(l.sheets.len(), 2);
assert_eq!(pages(&l.sheets[0].front), vec![Some(8), Some(1)]); assert_eq!(pages(&l.sheets[0].back), vec![Some(2), Some(7)]);
assert_eq!(pages(&l.sheets[1].front), vec![Some(6), Some(3)]); assert_eq!(pages(&l.sheets[1].back), vec![Some(4), Some(5)]);
assert_complete(&l);
}
#[test]
fn saddle_six_pages_pads_to_eight_append() {
let l = plan(BindingStyle::SaddleStitch, 1, 6, BlankPolicy::Append);
assert_eq!(l.padded_pages, 8);
assert_eq!(pages(&l.sheets[0].front), vec![None, Some(1)]); assert_eq!(pages(&l.sheets[0].back), vec![Some(2), None]); assert_complete(&l);
}
#[test]
fn perfect_bound_multi_signature() {
let l = plan(BindingStyle::PerfectBound, 2, 16, BlankPolicy::Append);
assert_eq!(l.signatures, 2);
assert_eq!(l.sheets.len(), 4);
assert_eq!(l.sheets[2].signature, 1); assert_complete(&l);
}
#[test]
fn stab_is_sequential_one_up() {
let l = plan(BindingStyle::Stab, 1, 5, BlankPolicy::Append);
assert_eq!(l.columns_per_side, 1);
assert_eq!(l.sheets.len(), 3); assert_eq!(pages(&l.sheets[0].front), vec![Some(1)]);
assert_eq!(pages(&l.sheets[0].back), vec![Some(2)]);
assert_eq!(pages(&l.sheets[2].front), vec![Some(5)]);
assert_eq!(pages(&l.sheets[2].back), vec![None]); assert_complete(&l);
}
#[test]
fn prepend_and_balance_place_blanks() {
let pre = plan(BindingStyle::SaddleStitch, 1, 6, BlankPolicy::Prepend);
assert_eq!(pages(&pre.sheets[0].front), vec![Some(6), None]);
assert_complete(&pre);
let bal = plan(BindingStyle::Stab, 1, 5, BlankPolicy::Balance);
assert_complete(&bal); }
#[test]
fn property_every_page_once_across_styles_and_sizes() {
for &style in &[
BindingStyle::SaddleStitch,
BindingStyle::PerfectBound,
BindingStyle::Stab,
BindingStyle::Concertina,
] {
for pages in [1usize, 2, 3, 4, 7, 16, 17, 33, 100, 249] {
for &blank in &[
BlankPolicy::Append,
BlankPolicy::Prepend,
BlankPolicy::Balance,
] {
let l = plan(style, 3, pages, blank);
assert_complete(&l);
}
}
}
}
}