pub const POINTS_PER_INCH: f32 = 72.0;
pub const REFERENCE_DPI: f32 = 100.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RenderScale {
figure_width_in: f32,
figure_height_in: f32,
dpi: f32,
device_scale: f32,
}
impl RenderScale {
fn sanitize_positive(value: f32, fallback: f32) -> f32 {
if value.is_finite() && value > 0.0 {
value
} else {
fallback
}
}
pub fn new(dpi: f32) -> Self {
Self {
figure_width_in: 6.4,
figure_height_in: 4.8,
dpi: Self::sanitize_positive(dpi, REFERENCE_DPI),
device_scale: 1.0,
}
}
pub fn with_figure(mut self, figure_width_in: f32, figure_height_in: f32) -> Self {
self.figure_width_in = Self::sanitize_positive(figure_width_in, 6.4);
self.figure_height_in = Self::sanitize_positive(figure_height_in, 4.8);
self
}
pub fn with_device_scale(dpi: f32, device_scale: f32) -> Self {
Self::new(dpi).with_host_scale(device_scale)
}
pub fn from_canvas_size(width_px: u32, height_px: u32, dpi: f32) -> Self {
let dpi = Self::sanitize_positive(dpi, REFERENCE_DPI);
Self::new(dpi).with_figure(
px_to_in(width_px as f32, dpi),
px_to_in(height_px as f32, dpi),
)
}
pub fn with_host_scale(mut self, device_scale: f32) -> Self {
self.device_scale = Self::sanitize_positive(device_scale, 1.0);
self
}
pub fn from_reference_scale(scale: f32) -> Self {
Self::new(Self::sanitize_positive(scale, 1.0) * REFERENCE_DPI)
}
pub fn figure_width_in(self) -> f32 {
self.figure_width_in
}
pub fn figure_height_in(self) -> f32 {
self.figure_height_in
}
pub fn dpi(self) -> f32 {
self.dpi
}
pub fn device_scale(self) -> f32 {
self.device_scale
}
pub fn device_dpi(self) -> f32 {
self.dpi * self.device_scale
}
pub fn reference_scale(self) -> f32 {
self.dpi / REFERENCE_DPI
}
pub fn points_to_pixels(self, points: f32) -> f32 {
pt_to_px(points, self.dpi)
}
pub fn inches_to_pixels(self, inches: f32) -> f32 {
in_to_px(inches, self.dpi)
}
pub fn pixels_to_inches(self, pixels: f32) -> f32 {
px_to_in(pixels, self.dpi)
}
pub fn pixels_to_points(self, pixels: f32) -> f32 {
px_to_pt(pixels, self.dpi)
}
pub fn logical_pixels_to_pixels(self, logical_pixels: f32) -> f32 {
logical_pixels * self.reference_scale()
}
pub fn pixels_to_logical_pixels(self, pixels: f32) -> f32 {
pixels / self.reference_scale()
}
pub fn pixels_to_device_pixels(self, pixels: f32) -> f32 {
pixels * self.device_scale
}
pub fn logical_pixels_to_device_pixels(self, logical_pixels: f32) -> f32 {
logical_pixels * self.device_scale
}
pub fn device_pixels_to_logical_pixels(self, pixels: f32) -> f32 {
pixels / self.device_scale
}
pub fn reference_pixels_to_pixels(self, pixels: f32) -> f32 {
pixels * self.reference_scale()
}
pub fn reference_pixels_to_device_pixels(self, pixels: f32) -> f32 {
self.pixels_to_device_pixels(self.reference_pixels_to_pixels(pixels))
}
pub fn canvas_size(self) -> (u32, u32) {
(
self.inches_to_pixels(self.figure_width_in) as u32,
self.inches_to_pixels(self.figure_height_in) as u32,
)
}
pub fn device_canvas_size(self) -> (u32, u32) {
(
self.pixels_to_device_pixels(self.inches_to_pixels(self.figure_width_in))
.round() as u32,
self.pixels_to_device_pixels(self.inches_to_pixels(self.figure_height_in))
.round() as u32,
)
}
pub fn canvas_size_pixels(self, width_in: f32, height_in: f32) -> (u32, u32) {
(
self.inches_to_pixels(width_in) as u32,
self.inches_to_pixels(height_in) as u32,
)
}
pub fn canvas_size_device_pixels(self, width_in: f32, height_in: f32) -> (u32, u32) {
(
self.pixels_to_device_pixels(self.inches_to_pixels(width_in))
.round() as u32,
self.pixels_to_device_pixels(self.inches_to_pixels(height_in))
.round() as u32,
)
}
}
#[inline]
pub fn pt_to_px(points: f32, dpi: f32) -> f32 {
points * dpi / POINTS_PER_INCH
}
#[inline]
pub fn in_to_px(inches: f32, dpi: f32) -> f32 {
inches * dpi
}
#[inline]
pub fn px_to_in(pixels: f32, dpi: f32) -> f32 {
pixels / dpi
}
#[inline]
pub fn px_to_pt(pixels: f32, dpi: f32) -> f32 {
pixels * POINTS_PER_INCH / dpi
}
#[inline]
pub fn pt_to_in(points: f32) -> f32 {
points / POINTS_PER_INCH
}
#[inline]
pub fn in_to_pt(inches: f32) -> f32 {
inches * POINTS_PER_INCH
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_scale_logical_and_device_conversion() {
let scale = RenderScale::with_device_scale(150.0, 2.0);
assert!((scale.points_to_pixels(12.0) - 25.0).abs() < 0.001);
assert!((scale.pixels_to_device_pixels(25.0) - 50.0).abs() < 0.001);
assert_eq!(scale.canvas_size_pixels(6.4, 4.8), (960, 720));
assert_eq!(scale.canvas_size_device_pixels(6.4, 4.8), (1920, 1440));
}
#[test]
fn test_render_scale_reference_baseline_conversion() {
let scale = RenderScale::new(300.0);
assert!((scale.logical_pixels_to_pixels(5.0) - 15.0).abs() < 0.001);
assert!((scale.reference_scale() - 3.0).abs() < 0.001);
}
#[test]
fn test_pt_to_px() {
assert!((pt_to_px(10.0, 72.0) - 10.0).abs() < 0.001);
assert!((pt_to_px(10.0, 144.0) - 20.0).abs() < 0.001);
assert!((pt_to_px(10.0, 100.0) - 13.889).abs() < 0.01);
}
#[test]
fn test_in_to_px() {
assert!((in_to_px(6.4, 100.0) - 640.0).abs() < 0.001);
assert!((in_to_px(6.4, 300.0) - 1920.0).abs() < 0.001);
}
#[test]
fn test_px_to_in() {
assert!((px_to_in(640.0, 100.0) - 6.4).abs() < 0.001);
assert!((px_to_in(1920.0, 300.0) - 6.4).abs() < 0.001);
}
#[test]
fn test_px_to_pt() {
assert!((px_to_pt(10.0, 72.0) - 10.0).abs() < 0.001);
assert!((px_to_pt(20.0, 144.0) - 10.0).abs() < 0.001);
}
#[test]
fn test_pt_in_roundtrip() {
let original_pt = 36.0;
let inches = pt_to_in(original_pt);
let back_to_pt = in_to_pt(inches);
assert!((original_pt - back_to_pt).abs() < 0.001);
}
#[test]
fn test_dpi_independence() {
let font_pt = 10.0;
let figure_in = 6.4;
let font_px_100 = pt_to_px(font_pt, 100.0);
let figure_px_100 = in_to_px(figure_in, 100.0);
let ratio_100 = font_px_100 / figure_px_100;
let font_px_300 = pt_to_px(font_pt, 300.0);
let figure_px_300 = in_to_px(figure_in, 300.0);
let ratio_300 = font_px_300 / figure_px_300;
assert!((ratio_100 - ratio_300).abs() < 0.0001);
}
#[test]
fn test_render_scale_points_and_inches() {
let scale = RenderScale::new(144.0);
assert!((scale.points_to_pixels(10.0) - 20.0).abs() < 0.001);
assert!((scale.inches_to_pixels(2.0) - 288.0).abs() < 0.001);
assert!((scale.pixels_to_points(20.0) - 10.0).abs() < 0.001);
assert!((scale.pixels_to_inches(288.0) - 2.0).abs() < 0.001);
}
#[test]
fn test_render_scale_logical_pixels() {
let scale = RenderScale::new(200.0);
assert!((scale.logical_pixels_to_pixels(10.0) - 20.0).abs() < 0.001);
assert!((scale.pixels_to_logical_pixels(20.0) - 10.0).abs() < 0.001);
}
#[test]
fn test_render_scale_device_scale_is_separate() {
let scale = RenderScale::with_device_scale(150.0, 2.0);
assert!((scale.points_to_pixels(12.0) - 25.0).abs() < 0.001);
assert!((scale.logical_pixels_to_device_pixels(10.0) - 20.0).abs() < 0.001);
assert!((scale.pixels_to_device_pixels(15.0) - 30.0).abs() < 0.001);
}
#[test]
fn test_render_scale_sanitizes_invalid_inputs() {
let scale = RenderScale::with_device_scale(f32::NAN, 0.0);
assert!((scale.dpi() - REFERENCE_DPI).abs() < 0.001);
assert!((scale.device_scale() - 1.0).abs() < 0.001);
}
}