mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 06:42:20 +00:00
fix(measure): validate written Y factor and check /F and /D on last format only
This commit is contained in:
1 parent
c2355b0b0e
commit
4439794769
3 files changed
+56
-32
No files matched your search
@@ -157,6 +157,12 @@ impl Scale {
|
||||
}
|
||||
if let Some(formats) = &self.formats {
|
||||
formats.validate().map_err(invalid)?;
|
||||
if formats.y.is_some() {
|
||||
let yf = self.y / formats.cyx;
|
||||
if !yf.is_finite() || !(1e-12..=1e12).contains(&yf) {
|
||||
return Err(invalid("scale factors must be finite and between 1e-12 and 1e12"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -187,33 +187,40 @@ pub(crate) fn read(doc: &Document, object: &Object) -> Result<Vec<NumberFormat>,
|
||||
if a.len() > MAX_UNITS {
|
||||
return Err("number formats of more than 8 units are not supported");
|
||||
}
|
||||
a.iter().map(|o| read_one(doc, &doc.resolve(o))).collect()
|
||||
a.iter().enumerate().map(|(i, o)| read_one(doc, &doc.resolve(o), i + 1 == a.len())).collect()
|
||||
}
|
||||
|
||||
fn read_one(doc: &Document, o: &Object) -> Result<NumberFormat, &'static str> {
|
||||
fn read_one(doc: &Document, o: &Object, is_last: bool) -> Result<NumberFormat, &'static str> {
|
||||
let f = o.as_dict().ok_or("invalid number format dictionary")?;
|
||||
let get = |k: &[u8]| f.get(k).map(|v| doc.resolve(v));
|
||||
let text = |k: &[u8], default: &str| get(k).and_then(|v| v.as_string().map(PdfString::to_text)).unwrap_or_else(|| default.into());
|
||||
let remainder = match get(b"F").as_deref().and_then(Object::as_name) {
|
||||
None | Some(b"D") => Remainder::Decimal,
|
||||
Some(b"F") => Remainder::Fraction,
|
||||
Some(b"R") => Remainder::Round,
|
||||
Some(b"T") => Remainder::Truncate,
|
||||
Some(_) => return Err("unknown measurement number format"),
|
||||
// ISO 32000-2 §12.9.2: only the last element formats a remainder (`/F`, `/D`, `/FD`);
|
||||
// earlier elements always format whole units.
|
||||
let (remainder, denominator, exact) = if is_last {
|
||||
let remainder = match get(b"F").as_deref().and_then(Object::as_name) {
|
||||
None | Some(b"D") => Remainder::Decimal,
|
||||
Some(b"F") => Remainder::Fraction,
|
||||
Some(b"R") => Remainder::Round,
|
||||
Some(b"T") => Remainder::Truncate,
|
||||
Some(_) => return Err("unknown measurement number format"),
|
||||
};
|
||||
let denominator = match remainder {
|
||||
Remainder::Decimal => get(b"D").and_then(|v| v.as_int()).unwrap_or(100),
|
||||
Remainder::Fraction => get(b"D").and_then(|v| v.as_int()).unwrap_or(16),
|
||||
Remainder::Round | Remainder::Truncate => 1,
|
||||
};
|
||||
let denominator_ok = match remainder {
|
||||
Remainder::Decimal => (0..=6).any(|p| 10_i64.pow(p) == denominator),
|
||||
Remainder::Fraction => (1..=MAX_DENOMINATOR).contains(&denominator),
|
||||
Remainder::Round | Remainder::Truncate => true,
|
||||
};
|
||||
if !denominator_ok {
|
||||
return Err("unsupported measurement precision");
|
||||
}
|
||||
(remainder, denominator, matches!(get(b"FD").as_deref(), Some(Object::Bool(true))))
|
||||
} else {
|
||||
(Remainder::Decimal, 100, false)
|
||||
};
|
||||
let denominator = match remainder {
|
||||
Remainder::Decimal => get(b"D").and_then(|v| v.as_int()).unwrap_or(100),
|
||||
Remainder::Fraction => get(b"D").and_then(|v| v.as_int()).unwrap_or(16),
|
||||
Remainder::Round | Remainder::Truncate => 1,
|
||||
};
|
||||
let denominator_ok = match remainder {
|
||||
Remainder::Decimal => (0..=6).any(|p| 10_i64.pow(p) == denominator),
|
||||
Remainder::Fraction => (1..=MAX_DENOMINATOR).contains(&denominator),
|
||||
Remainder::Round | Remainder::Truncate => true,
|
||||
};
|
||||
if !denominator_ok {
|
||||
return Err("unsupported measurement precision");
|
||||
}
|
||||
// An empty RD means the default decimal point.
|
||||
let decimal = Some(text(b"RD", ".")).filter(|s| !s.is_empty()).unwrap_or_else(|| ".".into());
|
||||
Ok(NumberFormat {
|
||||
@@ -221,7 +228,7 @@ fn read_one(doc: &Document, o: &Object) -> Result<NumberFormat, &'static str> {
|
||||
factor: get(b"C").and_then(|v| v.as_f64()).ok_or("missing conversion factor")?,
|
||||
remainder,
|
||||
denominator,
|
||||
exact: matches!(get(b"FD").as_deref(), Some(Object::Bool(true))),
|
||||
exact,
|
||||
thousands: text(b"RT", ","),
|
||||
decimal,
|
||||
before: text(b"PS", " "),
|
||||
@@ -231,13 +238,13 @@ fn read_one(doc: &Document, o: &Object) -> Result<NumberFormat, &'static str> {
|
||||
}
|
||||
|
||||
/// Checks an array: 1 to [`MAX_UNITS`] elements, factors as a scale's, labels and separators of
|
||||
/// at most 24 printable characters, a decimal precision of 0 to 6 places and a denominator of 1
|
||||
/// to 65,536.
|
||||
/// at most 24 printable characters, and on the last element a decimal precision of 0 to 6 places
|
||||
/// or a fraction denominator of 1 to 65,536.
|
||||
fn validate(formats: &[NumberFormat]) -> Result<(), &'static str> {
|
||||
if formats.is_empty() || formats.len() > MAX_UNITS {
|
||||
return Err("number formats have 1 to 8 units");
|
||||
}
|
||||
for f in formats {
|
||||
for (i, f) in formats.iter().enumerate() {
|
||||
if !f.factor.is_finite() || !(1e-12..=1e12).contains(&f.factor) {
|
||||
return Err("scale factors must be finite and between 1e-12 and 1e12");
|
||||
}
|
||||
@@ -247,13 +254,15 @@ fn validate(formats: &[NumberFormat]) -> Result<(), &'static str> {
|
||||
if [&f.thousands, &f.decimal, &f.before, &f.after].iter().any(|s| s.chars().count() > MAX_TEXT || s.chars().any(char::is_control)) {
|
||||
return Err("number format separators must be at most 24 printable characters");
|
||||
}
|
||||
let denominator_ok = match f.remainder {
|
||||
Remainder::Decimal => (0..=6).any(|p| 10_i64.pow(p) == f.denominator),
|
||||
Remainder::Fraction => (1..=MAX_DENOMINATOR).contains(&f.denominator),
|
||||
Remainder::Round | Remainder::Truncate => f.denominator == 1,
|
||||
};
|
||||
if !denominator_ok {
|
||||
return Err("unsupported measurement precision");
|
||||
if i + 1 == formats.len() {
|
||||
let denominator_ok = match f.remainder {
|
||||
Remainder::Decimal => (0..=6).any(|p| 10_i64.pow(p) == f.denominator),
|
||||
Remainder::Fraction => (1..=MAX_DENOMINATOR).contains(&f.denominator),
|
||||
Remainder::Round | Remainder::Truncate => f.denominator == 1,
|
||||
};
|
||||
if !denominator_ok {
|
||||
return Err("unsupported measurement precision");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -506,6 +515,8 @@ mod tests {
|
||||
assert_eq!(parsed(&[], source).and_then(|f| validate(&f).map(|()| f)), Err(reason), "{source}");
|
||||
}
|
||||
assert_eq!(parsed(&[], "[<< /U (ft) /C 1 /F /F /D 65536 >>]").unwrap()[0].denominator, 65536);
|
||||
// Only the last element's `/F` and `/D` format a remainder; earlier elements' `/F` and `/D` are ignored.
|
||||
assert_eq!(parsed(&[], "[<< /U (ft) /C 1 /F /X /D 12 >> << /U (in) /C 12 /F /F /D 16 >>]").map(|f| f.len()), Ok(2));
|
||||
assert_eq!(parsed(&[], &units(MAX_UNITS)).map(|f| f.len()), Ok(MAX_UNITS));
|
||||
}
|
||||
|
||||
|
||||
@@ -701,4 +701,11 @@ fn a_compound_y_keeps_its_units_and_cyx() {
|
||||
close(again.y, scale.y);
|
||||
let formats = again.formats.unwrap();
|
||||
assert_eq!((formats.y.unwrap()[1].factor, formats.cyx), (100.0, 3.28084));
|
||||
// If `Scale.y` is mutated so `self.y / cyx` exceeds the factor bound, `dictionary()` refuses it.
|
||||
let mut overflow_y = scale;
|
||||
if let Some(f) = &mut overflow_y.formats {
|
||||
f.cyx = 1e-12;
|
||||
}
|
||||
overflow_y.y = 2.0;
|
||||
assert!(overflow_y.dictionary().is_err());
|
||||
}
|
||||
Reference in new issue
Block a user