use crate::util::RasterDimensions;
pub(crate) const CSS_REFERENCE_DPI: f32 = 96.0;
pub(crate) const DEFAULT_SOURCE_IMAGE_DPI: f32 = 300.0;
pub(crate) const DEFAULT_FILTER_RASTER_DPI: f32 = 300.0;
pub(crate) const DEFAULT_MASK_RASTER_DPI: f32 = 300.0;
pub(crate) const DEFAULT_BACKGROUND_RASTER_DPI: f32 = 192.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct JpegCompression {
quality: u8,
}
impl JpegCompression {
pub const fn new(quality: u8) -> Self {
Self {
quality: if quality > 100 { 100 } else { quality },
}
}
pub(crate) const fn quality(self) -> u8 {
self.quality
}
}
impl Default for JpegCompression {
fn default() -> Self {
Self::new(50)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CoverageCompression {
Lossless,
Jpeg(JpegCompression),
}
impl Default for CoverageCompression {
fn default() -> Self {
Self::Jpeg(JpegCompression::default())
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RasterQuality {
pub source_image_dpi: f32,
pub filter_dpi: f32,
pub mask_dpi: f32,
pub background_dpi: f32,
pub blurred_coverage_compression: CoverageCompression,
}
impl Default for RasterQuality {
fn default() -> Self {
Self {
source_image_dpi: DEFAULT_SOURCE_IMAGE_DPI,
filter_dpi: DEFAULT_FILTER_RASTER_DPI,
mask_dpi: DEFAULT_MASK_RASTER_DPI,
background_dpi: DEFAULT_BACKGROUND_RASTER_DPI,
blurred_coverage_compression: CoverageCompression::default(),
}
}
}
impl RasterQuality {
pub(crate) fn normalized(self) -> Self {
Self {
source_image_dpi: raster_dpi_at_least(self.source_image_dpi, 72.0),
filter_dpi: raster_dpi_at_least(self.filter_dpi, 1.0),
mask_dpi: raster_dpi_at_least(self.mask_dpi, 72.0),
background_dpi: raster_dpi_at_least(self.background_dpi, CSS_REFERENCE_DPI),
blurred_coverage_compression: self.blurred_coverage_compression,
}
}
}
pub(crate) fn raster_dpi_at_least(dpi: f32, minimum: f32) -> f32 {
if dpi.is_finite() {
dpi.max(minimum)
} else {
minimum
}
}
fn raster_dimensions_at_dpi(
width: f32,
height: f32,
dpi: f32,
minimum_dpi: f32,
) -> Option<RasterDimensions> {
let dpi = raster_dpi_at_least(dpi, minimum_dpi);
RasterDimensions::scaled_points(width, height, dpi / CSS_REFERENCE_DPI)
}
pub(crate) fn filter_raster_dimensions(
width: f32,
height: f32,
dpi: f32,
) -> Option<RasterDimensions> {
raster_dimensions_at_dpi(width, height, dpi, 1.0)
}
pub(crate) fn mask_raster_dimensions(
width: f32,
height: f32,
dpi: f32,
) -> Option<RasterDimensions> {
let dpi = raster_dpi_at_least(dpi, 72.0);
RasterDimensions::scaled_points_ceil(width, height, dpi / CSS_REFERENCE_DPI)
}
pub(crate) fn background_raster_dimensions(
width: f32,
height: f32,
dpi: f32,
) -> Option<RasterDimensions> {
raster_dimensions_at_dpi(width, height, dpi, CSS_REFERENCE_DPI)
}
#[cfg(test)]
mod tests {
use super::*;
const BLENDED_BACKGROUND: &str = r#"
<style>
@page { size: 224px 144px; margin: 0; }
* { margin: 0; box-sizing: border-box; }
.box {
width: 220px;
height: 140px;
background-color: #fdd835;
background-image:
linear-gradient(to right, transparent 0 50%, #d32f2f 50% 100%),
linear-gradient(to bottom, #1565c0 0 50%, transparent 50% 100%);
background-blend-mode: multiply, screen;
}
</style>
<div class="box"></div>
"#;
fn has_image_dimensions(pdf: &[u8], width: u32, height: u32) -> bool {
let content = String::from_utf8_lossy(pdf);
content.contains(&format!("/Subtype /Image /Width {width} /Height {height}"))
}
#[test]
fn default_uses_the_tested_physical_background_baseline() {
assert_eq!(
background_raster_dimensions(72.0, 36.0, DEFAULT_BACKGROUND_RASTER_DPI),
Some(RasterDimensions {
width: 192,
height: 96,
})
);
}
#[test]
fn raster_quality_defaults_are_one_named_resolution_contract() {
let quality = RasterQuality::default();
assert_eq!(quality.source_image_dpi, DEFAULT_SOURCE_IMAGE_DPI);
assert_eq!(quality.filter_dpi, DEFAULT_FILTER_RASTER_DPI);
assert_eq!(quality.mask_dpi, DEFAULT_MASK_RASTER_DPI);
assert_eq!(quality.background_dpi, DEFAULT_BACKGROUND_RASTER_DPI);
assert_eq!(
quality.blurred_coverage_compression,
CoverageCompression::Jpeg(JpegCompression::new(50))
);
}
#[test]
fn non_finite_resolution_uses_the_semantic_minimum() {
assert_eq!(raster_dpi_at_least(f32::NAN, 72.0), 72.0);
assert_eq!(raster_dpi_at_least(f32::INFINITY, 96.0), 96.0);
assert_eq!(raster_dpi_at_least(150.0, 96.0), 150.0);
}
#[test]
fn dimensions_use_explicit_physical_dpi() {
assert_eq!(
background_raster_dimensions(72.0, 36.0, 300.0),
Some(RasterDimensions {
width: 300,
height: 150,
})
);
}
#[test]
fn mask_dimensions_use_coverage_preserving_rounding() {
assert_eq!(
mask_raster_dimensions(150.0, 150.0, DEFAULT_MASK_RASTER_DPI),
Some(RasterDimensions {
width: 625,
height: 625,
})
);
assert_eq!(
mask_raster_dimensions(150.0, 150.0, 150.0),
Some(RasterDimensions {
width: 313,
height: 313,
})
);
}
#[test]
fn filters_share_one_configurable_physical_resolution_policy() {
assert_eq!(
filter_raster_dimensions(72.0, 36.0, DEFAULT_FILTER_RASTER_DPI),
Some(RasterDimensions {
width: 300,
height: 150,
})
);
assert_eq!(
filter_raster_dimensions(72.0, 36.0, f32::NAN),
Some(RasterDimensions {
width: 1,
height: 1,
})
);
}
#[test]
fn converter_raster_quality_scales_flattened_background_images() {
let at_css_dpi = crate::HtmlConverter::new()
.raster_quality(RasterQuality {
background_dpi: 96.0,
..RasterQuality::default()
})
.convert(BLENDED_BACKGROUND)
.unwrap();
let at_print_dpi = crate::HtmlConverter::new()
.background_raster_dpi(300.0)
.convert(BLENDED_BACKGROUND)
.unwrap();
assert!(has_image_dimensions(&at_css_dpi, 220, 140));
assert!(has_image_dimensions(&at_print_dpi, 687, 437));
}
}