mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-14 06:42:20 +00:00
fix(measure): nudge whole units by a step of the last unit; keep Y's own units and CYX
A whole-unit nudge measured in the larger unit could exceed a step of the last unit (1 km - 0.9 mm showed as 1 km 0 m). A compound Y in units other than X's was relabelled with X's unit and lost its CYX when written back.
This commit is contained in:
1 parent
87d8deaced
commit
c2355b0b0e
3 files changed
+68
-13
No files matched your search
@@ -69,6 +69,8 @@ pub struct Scale {
|
||||
pub distance_factor: f64,
|
||||
pub area_factor: f64,
|
||||
pub area_unit: String,
|
||||
/// Decimal places of a distance. With [`Scale::formats`], those of the last distance unit
|
||||
/// when it shows decimals (0 otherwise); the formats, not this, label the readings.
|
||||
pub precision: u8,
|
||||
pub ratio: String,
|
||||
/// The number format arrays of an imported scale whose units are compound (feet and inches)
|
||||
@@ -184,9 +186,18 @@ impl Scale {
|
||||
d.set(b"R".to_vec(), PdfString::text(&self.ratio));
|
||||
if let Some(f) = &self.formats {
|
||||
d.set(b"X".to_vec(), imported(&f.x, &self.unit, self.x));
|
||||
if self.x != self.y {
|
||||
d.set(b"Y".to_vec(), imported(f.y.as_deref().unwrap_or(&f.x), &self.unit, self.y));
|
||||
d.set(b"CYX".to_vec(), Object::Real(1.0));
|
||||
match &f.y {
|
||||
// Y keeps its own units, and the CYX that converts them to X's.
|
||||
Some(ys) => {
|
||||
let unit = ys.first().map_or(self.unit.as_str(), |f| f.unit.as_str());
|
||||
d.set(b"Y".to_vec(), imported(ys, unit, self.y / f.cyx));
|
||||
d.set(b"CYX".to_vec(), Object::Real(f.cyx));
|
||||
}
|
||||
None if self.x != self.y => {
|
||||
d.set(b"Y".to_vec(), imported(&f.x, &self.unit, self.y));
|
||||
d.set(b"CYX".to_vec(), Object::Real(1.0));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
d.set(b"D".to_vec(), imported(&f.distance, &self.unit, self.distance_factor));
|
||||
d.set(b"A".to_vec(), imported(&f.area, &self.area_unit, self.area_factor));
|
||||
@@ -220,12 +231,12 @@ impl Scale {
|
||||
let xs = format(b"X")?;
|
||||
let (unit, x) = first(&xs)?;
|
||||
let ys = if d.contains(b"Y") { Some(format(b"Y")?) } else { None };
|
||||
let y = match &ys {
|
||||
let (y, cyx) = match &ys {
|
||||
Some(ys) => {
|
||||
first(ys)?.1
|
||||
* d.get(b"CYX").and_then(|o| doc.resolve(o).as_f64()).ok_or_else(|| invalid("Y scale requires CYX for lengths and areas"))?
|
||||
let cyx = d.get(b"CYX").and_then(|o| doc.resolve(o).as_f64()).ok_or_else(|| invalid("Y scale requires CYX for lengths and areas"))?;
|
||||
(first(ys)?.1 * cyx, cyx)
|
||||
}
|
||||
None => x,
|
||||
None => (x, 1.0),
|
||||
};
|
||||
let distances = format(b"D")?;
|
||||
let (distance_unit, distance_factor) = first(&distances)?;
|
||||
@@ -244,7 +255,7 @@ impl Scale {
|
||||
.and_then(|f| u8::try_from(f.denominator.max(1).ilog10()).ok())
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let formats = (!plain).then_some(Formats { x: xs, y: ys, distance: distances, area: areas });
|
||||
let formats = (!plain).then_some(Formats { x: xs, y: ys, cyx, distance: distances, area: areas });
|
||||
let out = Self { x, y, unit, distance_factor, area_factor, area_unit, precision, ratio: text(doc, d, b"R"), formats };
|
||||
out.validate()?;
|
||||
Ok(out)
|
||||
|
||||
@@ -17,8 +17,8 @@ pub const MAX_UNITS: usize = 8;
|
||||
const MAX_TEXT: usize = 24;
|
||||
/// Largest denominator of a fraction (`F /F`).
|
||||
const MAX_DENOMINATOR: i64 = 1 << 16;
|
||||
/// A remainder this close below a whole step counts as the step: `3.5 ft` stored as
|
||||
/// `3.4999999999 ft` still cuts to 6 in, not 5 in.
|
||||
/// A remainder this close below a whole step of the last unit, in steps, counts as the step:
|
||||
/// `3.5 ft` stored as `3.4999999999 ft` still cuts to 6 in, not 5 in.
|
||||
const NUDGE: f64 = 1e-6;
|
||||
|
||||
/// How the part of the last unit below a whole one shows (`F`).
|
||||
@@ -128,9 +128,15 @@ impl NumberFormat {
|
||||
/// for: compound units (feet and inches), fractions, or values cut off to whole units.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
pub struct Formats {
|
||||
/// `X`: changes along the x axis (and the y axis when there is no `Y`).
|
||||
pub x: Vec<NumberFormat>,
|
||||
/// `Y`: changes along the y axis, in its own units, as read.
|
||||
pub y: Option<Vec<NumberFormat>>,
|
||||
/// `CYX`: y-axis units in one x-axis unit, as read with `y` (1 without it).
|
||||
pub cyx: f64,
|
||||
/// `D`: distances.
|
||||
pub distance: Vec<NumberFormat>,
|
||||
/// `A`: areas.
|
||||
pub area: Vec<NumberFormat>,
|
||||
}
|
||||
|
||||
@@ -142,6 +148,9 @@ impl Formats {
|
||||
|
||||
/// Checks every array as [`crate::Scale::validate`] checks a scale's own fields.
|
||||
pub(crate) fn validate(&self) -> Result<(), &'static str> {
|
||||
if !self.cyx.is_finite() || !(1e-12..=1e12).contains(&self.cyx) {
|
||||
return Err("scale factors must be finite and between 1e-12 and 1e12");
|
||||
}
|
||||
self.arrays().try_for_each(|a| validate(a))
|
||||
}
|
||||
|
||||
@@ -259,14 +268,17 @@ pub(crate) fn format(value: f64, formats: &[NumberFormat]) -> String {
|
||||
if !value.is_finite() {
|
||||
return String::new();
|
||||
}
|
||||
let steps = last.steps();
|
||||
let mut wholes = Vec::with_capacity(formats.len());
|
||||
let mut rest = value.abs();
|
||||
for next in formats.iter().skip(1) {
|
||||
let whole = (rest + NUDGE).floor();
|
||||
for (i, next) in formats.iter().enumerate().skip(1) {
|
||||
// The nudge is a fraction of a step of the last unit, however many units lie between.
|
||||
let per_step = formats.iter().skip(i).fold(steps, |p, f| p * f.factor);
|
||||
let nudge = if per_step.is_finite() && per_step > 1.0 { NUDGE / per_step } else { NUDGE };
|
||||
let whole = (rest + nudge).floor();
|
||||
wholes.push(whole);
|
||||
rest = ((rest - whole) * next.factor).max(0.0);
|
||||
}
|
||||
let steps = last.steps();
|
||||
let mut count = if last.remainder == Remainder::Truncate { (rest * steps + NUDGE).floor() } else { (rest * steps).round() };
|
||||
carry(&mut count, steps, &mut wholes, formats);
|
||||
let mut out = String::new();
|
||||
@@ -403,6 +415,12 @@ mod tests {
|
||||
assert_eq!(format(1.9, &odd), "1 a 2 b");
|
||||
// A whole unit stored a hair below itself is still whole, factor whole or not.
|
||||
assert_eq!(format(2.0 - 1e-10, &odd), "2 a 0 b");
|
||||
// ... but a value a step of the last unit short of it is not, however small the step.
|
||||
let metres = vec![unit("km", 1.0, Remainder::Decimal, 100), unit("m", 1000.0, Remainder::Decimal, 1000)];
|
||||
assert_eq!(format(1.0 - 9e-7, &metres), "0 km 999.999 m");
|
||||
let miles =
|
||||
vec![unit("mi", 1.0, Remainder::Decimal, 100), unit("ft", 5280.0, Remainder::Decimal, 100), unit("in", 12.0, Remainder::Fraction, 64)];
|
||||
assert_eq!(format(1.0 - 9e-7, &miles), "0 mi 5,279 ft 11 15/16 in");
|
||||
// A value stored a hair below a whole step still cuts to it.
|
||||
let cut = vec![unit("ft", 1.0, Remainder::Decimal, 100), unit("in", 12.0, Remainder::Truncate, 1)];
|
||||
assert_eq!(format(3.4999999999, &cut), "3 ft 6 in");
|
||||
@@ -497,6 +515,7 @@ mod tests {
|
||||
let f = Formats {
|
||||
x: feet_inches(16),
|
||||
y: Some(feet_inches(16)),
|
||||
cyx: 1.0,
|
||||
distance: feet_inches(16),
|
||||
area: vec![unit("ft²", 1.0, Remainder::Decimal, 100)],
|
||||
};
|
||||
@@ -504,6 +523,8 @@ mod tests {
|
||||
let elements = 3 * 2 + 1;
|
||||
assert_eq!(f.held_bytes(), 3 * ((2 + 4) + (2 + 4)) + ("ft²".len() + 4) + elements * std::mem::size_of::<NumberFormat>());
|
||||
assert_eq!(f.validate(), Ok(()));
|
||||
let bad = Formats { cyx: 0.0, ..f.clone() };
|
||||
assert_eq!(bad.validate(), Err("scale factors must be finite and between 1e-12 and 1e12"));
|
||||
let bad = Formats { y: Some(Vec::new()), ..f };
|
||||
assert_eq!(bad.validate(), Err("number formats have 1 to 8 units"));
|
||||
}
|
||||
|
||||
@@ -679,3 +679,26 @@ fn compound_and_fractional_scales_read_label_and_carry_into_new_measurements() {
|
||||
// A plain decimal scale keeps no formats and labels as before.
|
||||
assert_eq!(Scale::read(&d, &Object::Dict(Scale::new(2.0, "m", 3).unwrap().dictionary().unwrap())).unwrap().formats, None);
|
||||
}
|
||||
|
||||
/// A compound Y in units of its own (metres and centimetres against feet and inches) is written
|
||||
/// back as read, with its CYX, so a viewer showing y coordinates reads the same units.
|
||||
#[test]
|
||||
fn a_compound_y_keeps_its_units_and_cyx() {
|
||||
let x = "[<< /U (ft) /C 0.0138888888888889 >> << /U (in) /C 12 /F /F /D 16 >>]";
|
||||
let y = "[<< /U (m) /C 0.000352778 >> << /U (cm) /C 100 /D 10 >>]";
|
||||
let measure = format!("<< /Type /Measure /Subtype /RL /X {x} /Y {y} /CYX 3.28084 /D {x} /A [<< /U (sq ft) /C 1 >>] >>");
|
||||
let d = fixture("", &format!("/VP [<< /Type /Viewport /BBox [0 0 612 792] /Measure {measure} >>]"), &[]);
|
||||
let scale = scale_at(&d, 0, [10.0, 10.0]).unwrap();
|
||||
close(scale.y, 0.000352778 * 3.28084);
|
||||
let written = scale.dictionary().unwrap();
|
||||
close(written.get(b"CYX").and_then(Object::as_f64).unwrap(), 3.28084);
|
||||
let ys = written.get(b"Y").and_then(Object::as_array).unwrap();
|
||||
let first = ys[0].as_dict().unwrap();
|
||||
assert_eq!(text(&d, first, b"U"), "m");
|
||||
close(first.get(b"C").and_then(Object::as_f64).unwrap(), 0.000352778);
|
||||
assert_eq!(text(&d, ys[1].as_dict().unwrap(), b"U"), "cm");
|
||||
let again = Scale::read(&d, &Object::Dict(written)).unwrap();
|
||||
close(again.y, scale.y);
|
||||
let formats = again.formats.unwrap();
|
||||
assert_eq!((formats.y.unwrap()[1].factor, formats.cyx), (100.0, 3.28084));
|
||||
}
|
||||
Reference in new issue
Block a user