From 1de7b44a8d57d17bc21183ba2a3344c81c504821 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 31 Oct 2026 05:33:56 +0100 Subject: [PATCH 1/2] Parse uppercase exponent in custom geometry path numbers Found while testing crates/pptx/src/custgeom.rs:334 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/pptx/src/custgeom.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/pptx/src/custgeom.rs b/crates/pptx/src/custgeom.rs index ae122144..81553026 100644 --- a/crates/pptx/src/custgeom.rs +++ b/crates/pptx/src/custgeom.rs @@ -331,13 +331,15 @@ pub fn parse_path(d: &str) -> Vec<(char, Vec)> { }; for ch in d.chars() { match ch { + // An 'E' inside a number is an exponent, not the end command. + 'E' if !token.is_empty() => token.push(ch), 'M' | 'L' | 'C' | 'Q' | 'Z' | 'm' | 'l' | 'c' | 'q' | 'z' | 'E' => { push_tok(&mut token, &mut nums); flush(cmd, &mut nums, &mut out); cmd = Some(ch.to_ascii_uppercase()); } ' ' | ',' | '\t' | '\n' => push_tok(&mut token, &mut nums), - '-' if !token.is_empty() && !token.ends_with('e') => { + '-' if !token.is_empty() && !token.ends_with(['e', 'E']) => { push_tok(&mut token, &mut nums); token.push(ch); } @@ -472,6 +474,13 @@ mod tests { assert_eq!(back[0].d, "M 0 0 L 100000 0 C 100000 10000 90000 50000 50000 50000 Q 0 50000 0 0 Z"); } + #[test] + fn parse_path_uppercase_exponent() { + assert_eq!(parse_path("M 1E-5 2 L 3 4"), vec![('M', vec![1e-5, 2.0]), ('L', vec![3.0, 4.0])]); + assert_eq!(parse_path("M 1.5E3 2"), vec![('M', vec![1500.0, 2.0])]); + assert_eq!(parse_path("M 1e-5 2"), vec![('M', vec![1e-5, 2.0])]); + } + #[test] fn parse_path_tokens() { let v = parse_path("M0,0 L10-5 Z"); From 3bf35c801fafc62a7d257da35097cd866c8eaec6 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 31 Oct 2026 00:51:36 -0400 Subject: [PATCH 2/2] Custom geometry: 'E' is an exponent only when digits follow, so 'M 0 0E' keeps its end command Co-Authored-By: Claude Opus 5.5 --- crates/pptx/src/custgeom.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/pptx/src/custgeom.rs b/crates/pptx/src/custgeom.rs index 81553026..981cb2e2 100644 --- a/crates/pptx/src/custgeom.rs +++ b/crates/pptx/src/custgeom.rs @@ -329,10 +329,19 @@ pub fn parse_path(d: &str) -> Vec<(char, Vec)> { token.clear(); } }; - for ch in d.chars() { + let cs: Vec = d.chars().collect(); + for (i, &ch) in cs.iter().enumerate() { match ch { - // An 'E' inside a number is an exponent, not the end command. - 'E' if !token.is_empty() => token.push(ch), + // An 'E' inside a number followed by a digit (or a sign and a digit) is an exponent, + // not the end command. + 'E' if !token.is_empty() && { + let n1 = cs.get(i + 1).copied().unwrap_or(' '); + let n2 = cs.get(i + 2).copied().unwrap_or(' '); + n1.is_ascii_digit() || ((n1 == '-' || n1 == '+') && n2.is_ascii_digit()) + } => + { + token.push(ch) + } 'M' | 'L' | 'C' | 'Q' | 'Z' | 'm' | 'l' | 'c' | 'q' | 'z' | 'E' => { push_tok(&mut token, &mut nums); flush(cmd, &mut nums, &mut out); @@ -479,6 +488,8 @@ mod tests { assert_eq!(parse_path("M 1E-5 2 L 3 4"), vec![('M', vec![1e-5, 2.0]), ('L', vec![3.0, 4.0])]); assert_eq!(parse_path("M 1.5E3 2"), vec![('M', vec![1500.0, 2.0])]); assert_eq!(parse_path("M 1e-5 2"), vec![('M', vec![1e-5, 2.0])]); + // An 'E' right after a number with no exponent digits is the end command. + assert_eq!(parse_path("M 0 0E"), vec![('M', vec![0.0, 0.0]), ('E', vec![])]); } #[test]