Files
photocraft/crates/doc/src/analysis.rs
T

156 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Image › Analysis data stored with a document (measurement scale, count groups, the ruler line)
//! and annotations (Notes). Pure data; the measuring itself lives in the engine.
use photocraft_color::Color;
use serde::{Deserialize, Serialize};
/// Image › Analysis › Set Measurement Scale: `pixel_length` pixels equal `logical_length`
/// `units`. The default is 1 pixel = 1 pixel.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct MeasurementScale {
pub pixel_length: f64,
pub logical_length: f64,
pub units: String,
}
impl Default for MeasurementScale {
fn default() -> Self {
Self { pixel_length: 1.0, logical_length: 1.0, units: "pixels".into() }
}
}
impl MeasurementScale {
/// Logical units per pixel.
pub fn factor(&self) -> f64 {
if self.pixel_length > 0.0 { self.logical_length / self.pixel_length } else { 1.0 }
}
pub fn is_default(&self) -> bool {
*self == Self::default()
}
/// Photoshop's "Scale" column text, e.g. `1 pixels = 1.0000 pixels`.
pub fn describe(&self) -> String {
// f64 Display already drops a whole number's `.0` (`5`, `0.5`) and, unlike an i64 cast,
// prints lengths past i64::MAX in full.
format!("{} pixels = {:.4} {}", self.pixel_length, self.logical_length, self.units)
}
}
/// One Count tool group: its numbered markers (document pixel coordinates, in click order).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct CountGroup {
pub name: String,
pub color: Color,
/// Marker (dot) size and label font size, in screen pixels (Photoshop: 1–10 and 8–72).
pub marker_size: u32,
pub label_size: u32,
pub visible: bool,
pub points: Vec<[f64; 2]>,
}
impl Default for CountGroup {
fn default() -> Self {
Self { name: "Count Group 1".into(), color: Color::rgb(1.0, 0.0, 0.0), marker_size: 1, label_size: 8, visible: true, points: Vec::new() }
}
}
/// The Ruler tool line, optionally with a protractor second arm (⌥-drag from an end point).
/// `start` is the vertex the angle is measured at when there is a protractor.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Ruler {
pub start: [f64; 2],
pub end: [f64; 2],
#[serde(default)]
pub protractor: Option<[f64; 2]>,
}
/// Measurement settings and marks of a document.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Measurement {
pub scale: MeasurementScale,
pub count_groups: Vec<CountGroup>,
/// Index into `count_groups` that new markers go to.
pub active_count_group: usize,
pub ruler: Option<Ruler>,
}
impl Measurement {
pub fn is_empty(&self) -> bool {
*self == Self::default()
}
/// Total markers over every group.
pub fn count_total(&self) -> usize {
self.count_groups.iter().map(|g| g.points.len()).sum()
}
}
/// A Note tool annotation (PSD `Anno` text annotation).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Note {
pub author: String,
pub color: Color,
pub text: String,
/// Icon position (top-left, document pixels).
pub position: [f64; 2],
/// Popup rectangle [left, top, right, bottom] (document pixels), as stored in PSD.
pub popup: [f64; 4],
/// Popup shown open.
pub open: bool,
/// Modification date as PDF date text (`D:YYYYMMDDhhmmss…`); empty when unknown.
pub modified: String,
}
impl Default for Note {
fn default() -> Self {
// Photoshop's default note colour (pale yellow).
Self {
author: String::new(),
color: Color::rgb(1.0, 1.0, 0.51),
text: String::new(),
position: [0.0, 0.0],
popup: [0.0, 0.0, 0.0, 0.0],
open: false,
modified: String::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scale_factor_and_text() {
let s = MeasurementScale { pixel_length: 100.0, logical_length: 2.5, units: "cm".into() };
assert!((s.factor() - 0.025).abs() < 1e-12);
assert_eq!(s.describe(), "100 pixels = 2.5000 cm");
assert!(MeasurementScale::default().is_default());
assert_eq!(MeasurementScale { pixel_length: 0.0, ..Default::default() }.factor(), 1.0);
}
#[test]
fn scale_text_prints_large_and_fractional_pixel_lengths() {
let big = MeasurementScale { pixel_length: 1e20, logical_length: 1.0, units: "mm".into() };
assert_eq!(big.describe(), "100000000000000000000 pixels = 1.0000 mm");
assert_eq!(MeasurementScale::default().describe(), "1 pixels = 1.0000 pixels");
let s = |p: f64| MeasurementScale { pixel_length: p, ..Default::default() }.describe();
assert_eq!(s(5.0), "5 pixels = 1.0000 pixels");
assert_eq!(s(0.5), "0.5 pixels = 1.0000 pixels");
assert_eq!(s(12.25), "12.25 pixels = 1.0000 pixels");
}
#[test]
fn serde_defaults_fill_missing_fields() {
let m: Measurement = serde_json::from_str("{}").unwrap();
assert!(m.is_empty());
let n: Note = serde_json::from_str(r#"{"text":"hi"}"#).unwrap();
assert_eq!(n.text, "hi");
let g = CountGroup { points: vec![[1.0, 2.0], [3.0, 4.0]], ..Default::default() };
let m = Measurement { count_groups: vec![g.clone(), g], ..Default::default() };
assert_eq!(m.count_total(), 4);
}
}