mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Reject out-of-range leading, baseline shift and scales in type styles (#6196)
Found while testing crates/engine/src/type_cmds.rs:287 Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
16774eea6f
commit
275d4cbcf7
@@ -89,6 +89,8 @@ pub const TYPE_SIZE_MIN_PT: f64 = 0.1;
|
||||
pub const TYPE_SIZE_MAX_PT: f64 = 1296.0;
|
||||
pub const TRACKING_MIN: f64 = -1000.0;
|
||||
pub const TRACKING_MAX: f64 = 10_000.0;
|
||||
/// Photoshop's horizontal and vertical scale range, in percent.
|
||||
pub const SCALE_MAX_PERCENT: f64 = 1000.0;
|
||||
|
||||
/// Validates the `kerning` key of character params: a number in [`KERN_MIN`]..=[`KERN_MAX`] or
|
||||
/// `"metrics"`, `"optical"`, `"off"` (`"none"`, `"0"`).
|
||||
@@ -104,7 +106,8 @@ pub fn check_kerning(p: &Value) -> std::result::Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates text metrics that can otherwise exceed the rasterizer's supported range.
|
||||
/// Validates text metrics (size, tracking, leading, baseline shift, scales) that can otherwise
|
||||
/// exceed the rasterizer's supported range or overflow `f32`, which a .pcraft manifest cannot hold.
|
||||
pub fn check_size_tracking(p: &Value) -> std::result::Result<(), String> {
|
||||
if let Some(v) = p.get("size") {
|
||||
match v.as_f64() {
|
||||
@@ -118,6 +121,30 @@ pub fn check_size_tracking(p: &Value) -> std::result::Result<(), String> {
|
||||
_ => return Err(format!("tracking must be between {TRACKING_MIN} and {TRACKING_MAX} (1/1000 em)")),
|
||||
}
|
||||
}
|
||||
if let Some(v) = p.get("leading") {
|
||||
let (min, max) = (f64::from(LEADING_MIN_PT), f64::from(LEADING_MAX_PT));
|
||||
match v {
|
||||
Value::Null => {}
|
||||
Value::String(a) if a == "auto" => {}
|
||||
Value::Number(n) if n.as_f64().is_some_and(|x| x.is_finite() && (min..=max).contains(&x)) => {}
|
||||
_ => return Err(format!("leading must be between {min} and {max} pt, or \"auto\"")),
|
||||
}
|
||||
}
|
||||
if let Some(v) = p.get("baselineShift") {
|
||||
let max = f64::from(BASELINE_SHIFT_MAX_PT);
|
||||
match v.as_f64() {
|
||||
Some(shift) if shift.is_finite() && shift.abs() <= max => {}
|
||||
_ => return Err(format!("baselineShift must be between -{max} and {max} pt")),
|
||||
}
|
||||
}
|
||||
for key in ["horizontalScale", "verticalScale"] {
|
||||
if let Some(v) = p.get(key) {
|
||||
match v.as_f64() {
|
||||
Some(scale) if scale.is_finite() && (0.0..=SCALE_MAX_PERCENT).contains(&scale) => {}
|
||||
_ => return Err(format!("{key} must be between 0 and {SCALE_MAX_PERCENT} %")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Character style numbers that overflow `f32` are refused, so the document stays saveable.
|
||||
|
||||
use photocraft_engine::{EngineError, Session};
|
||||
use serde_json::json;
|
||||
|
||||
fn fixture() -> (Session, u64) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", json!({"width": 40, "height": 30, "background": "white"})).unwrap();
|
||||
let id = s.execute("type.create", json!({"text": "Hi", "size": 24})).unwrap()["layer"].as_u64().unwrap();
|
||||
(s, id)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_range_style_numbers_are_refused() {
|
||||
for key in ["leading", "baselineShift", "horizontalScale", "verticalScale"] {
|
||||
let (mut s, id) = fixture();
|
||||
let past = s.active().unwrap().history.past_len();
|
||||
for value in [1e39, -1e39, 1e12] {
|
||||
let r = s.execute("type.setStyle", json!({"layer": id, key: value}));
|
||||
assert!(matches!(r, Err(EngineError::BadParams { .. })), "{key}={value}: {r:?}");
|
||||
}
|
||||
assert_eq!(s.active().unwrap().history.past_len(), past, "{key}: no history step");
|
||||
let r = s.execute("type.create", json!({"text": "x", key: 1e39}));
|
||||
assert!(matches!(r, Err(EngineError::BadParams { .. })), "create {key}: {r:?}");
|
||||
let dir = std::env::temp_dir().join(format!("pc_nonfinite_style_{key}_{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("t.pcraft");
|
||||
let saved = s.execute("file.saveACopy", json!({"path": path.to_string_lossy()}));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
saved.unwrap_or_else(|e| panic!("{key}: save failed: {e:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_range_style_numbers_still_apply_and_save() {
|
||||
let (mut s, id) = fixture();
|
||||
s.execute("type.setStyle", json!({"layer": id, "leading": 30, "baselineShift": -4, "horizontalScale": 120, "verticalScale": 80})).unwrap();
|
||||
s.execute("type.setStyle", json!({"layer": id, "leading": "auto"})).unwrap();
|
||||
let dir = std::env::temp_dir().join(format!("pc_nonfinite_style_ok_{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("t.pcraft");
|
||||
let saved = s.execute("file.saveACopy", json!({"path": path.to_string_lossy()}));
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
saved.unwrap_or_else(|e| panic!("save failed: {e:?}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_numeric_leading_is_refused() {
|
||||
let (mut s, id) = fixture();
|
||||
let past = s.active().unwrap().history.past_len();
|
||||
for value in [json!("18pt"), json!("Auto "), json!(true), json!([12])] {
|
||||
let r = s.execute("type.setStyle", json!({"layer": id, "leading": value}));
|
||||
assert!(matches!(r, Err(EngineError::BadParams { .. })), "leading={value}: {r:?}");
|
||||
}
|
||||
assert_eq!(s.active().unwrap().history.past_len(), past, "no history step");
|
||||
}
|
||||
Reference in New Issue
Block a user