use std::collections::BTreeSet;
use crate::types::{ PrintError, PrintSettings };
const CSS_PER_POINT: f64 = 96.0 / 72.0;
pub( super ) const PRINT_DPI: f64 = 200.0;
const MIN_DPI: f64 = 24.0;
const MAX_SIDE: f64 = 4096.0;
const MIN_AREA: f64 = 36.0;
#[ derive( Clone, Copy, Debug, PartialEq ) ]
pub( super ) struct Geometry
{
pub paper: ( f64, f64 ),
pub area: ( f64, f64, f64, f64 ),
pub css: ( f64, f64 ),
pub view: ( i32, i32 ),
pub zoom: f64,
}
pub( super ) fn preview_dpi( settings: &PrintSettings, width: u32 ) -> f64
{
let paper = settings.paper.0 as f64;
if !( paper > 0.0 ) { return PRINT_DPI; }
( width.max( 1 ) as f64 * 72.0 / paper ).clamp( MIN_DPI, PRINT_DPI )
}
pub( super ) fn geometry( settings: &PrintSettings, device_scale: f64, dpi: f64 ) -> Result<Geometry, PrintError>
{
let ( width, height ) = ( settings.paper.0 as f64, settings.paper.1 as f64 );
let [ top, right, bottom, left ] = settings.margins.map( |m| if m.is_finite() { m.max( 0.0 ) as f64 } else { 0.0 } );
let ( area_width, area_height ) = ( width - left - right, height - top - bottom );
if !( area_width >= MIN_AREA && area_height >= MIN_AREA )
{
return Err( PrintError::Failed( "the margins leave no room on the paper".to_string() ) );
}
let scale = if settings.scale.is_finite() { settings.scale.clamp( PrintSettings::MIN_SCALE, PrintSettings::MAX_SCALE ) as f64 } else { 1.0 };
let css = ( area_width * CSS_PER_POINT / scale, area_height * CSS_PER_POINT / scale );
let device = if device_scale.is_finite() { device_scale.max( 0.5 ) } else { 1.0 };
let mut zoom = area_width / 72.0 * dpi / device / css.0;
let longest = css.0.max( css.1 ) * zoom;
if longest > MAX_SIDE { zoom *= MAX_SIDE / longest; }
Ok( Geometry
{
paper: ( width, height ),
area: ( left, bottom, area_width, area_height ),
css,
view: ( ( css.0 * zoom ).round() as i32, ( css.1 * zoom ).round() as i32 ),
zoom,
} )
}
pub( super ) fn check( ranges: &[ ( u32, u32 ) ] ) -> Result<(), PrintError>
{
match ranges.iter().find( |( start, end )| start > end )
{
Some( ( start, end ) ) => Err( PrintError::Failed( format!( "the range {start}-{end} ends before it starts" ) ) ),
None => Ok( () ),
}
}
pub( super ) fn select( ranges: &[ ( u32, u32 ) ], total: usize ) -> Result<Vec<usize>, PrintError>
{
check( ranges )?;
let chosen: Vec<usize> = if ranges.is_empty()
{
( 0..total ).collect()
} else {
let mut set = BTreeSet::new();
for &( start, end ) in ranges
{
let first = start.max( 1 ) as usize - 1;
let last = ( end as usize ).min( total );
set.extend( first..last );
}
set.into_iter().collect()
};
if chosen.is_empty() { return Err( PrintError::Failed( "no page to print".to_string() ) ); }
Ok( chosen )
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn a4_lays_out_as_wide_as_other_browsers_print_it()
{
let g = geometry( &PrintSettings::default(), 1.0, PRINT_DPI ).unwrap();
assert!( ( g.css.0 - 718.1 ).abs() < 0.5, "{}", g.css.0 );
assert!( ( g.view.0 as f64 / g.zoom - g.css.0 ).abs() < 1.0 );
}
#[ test ]
fn the_image_is_as_sharp_whatever_the_device_scale()
{
let one = geometry( &PrintSettings::default(), 1.0, PRINT_DPI ).unwrap();
let two = geometry( &PrintSettings::default(), 2.0, PRINT_DPI ).unwrap();
assert!( ( one.view.0 as f64 - 2.0 * two.view.0 as f64 ).abs() <= 1.0 );
assert!( ( one.css.0 - two.css.0 ).abs() < f64::EPSILON );
}
#[ test ]
fn a_smaller_scale_lays_out_wider()
{
let half = geometry( &PrintSettings { scale: 0.5, ..PrintSettings::default() }, 1.0, PRINT_DPI ).unwrap();
let full = geometry( &PrintSettings::default(), 1.0, PRINT_DPI ).unwrap();
assert!( ( half.css.0 - 2.0 * full.css.0 ).abs() < 0.01 );
assert!( half.view.0 as f64 <= MAX_SIDE && half.view.1 as f64 <= MAX_SIDE );
}
#[ test ]
fn the_scale_is_kept_within_bounds()
{
let tiny = geometry( &PrintSettings { scale: 0.001, ..PrintSettings::default() }, 1.0, PRINT_DPI ).unwrap();
let low = geometry( &PrintSettings { scale: PrintSettings::MIN_SCALE, ..PrintSettings::default() }, 1.0, PRINT_DPI ).unwrap();
assert_eq!( tiny.css, low.css );
}
#[ test ]
fn margins_wider_than_the_paper_fail()
{
assert!( geometry( &PrintSettings { margins: [ 300.0; 4 ], ..PrintSettings::default() }, 1.0, PRINT_DPI ).is_err() );
}
#[ test ]
fn a_preview_is_as_wide_as_asked()
{
let settings = PrintSettings::default();
let g = geometry( &settings, 1.0, preview_dpi( &settings, 480 ) ).unwrap();
let sheet = g.view.0 as f64 * g.paper.0 / g.area.2;
assert!( ( sheet - 480.0 ).abs() < 2.0, "{sheet}" );
}
#[ test ]
fn ranges_print_in_order_once_each()
{
assert_eq!( select( &[ ( 5, 6 ), ( 1, 2 ), ( 2, 3 ) ], 10 ).unwrap(), vec![ 0, 1, 2, 4, 5 ] );
}
#[ test ]
fn no_ranges_print_everything()
{
assert_eq!( select( &[], 3 ).unwrap(), vec![ 0, 1, 2 ] );
}
#[ test ]
fn pages_past_the_end_are_left_out()
{
assert_eq!( select( &[ ( 2, 9 ) ], 3 ).unwrap(), vec![ 1, 2 ] );
assert!( select( &[ ( 5, 9 ) ], 3 ).is_err() );
}
#[ test ]
fn a_backwards_range_fails()
{
assert!( select( &[ ( 3, 1 ) ], 5 ).is_err() );
}
}