From 766ff9d05a51e943396d42de1dcf8d5c12ce4fbd Mon Sep 17 00:00:00 2001 From: Hamza Date: Tue, 20 Oct 2026 14:47:52 +0500 Subject: [PATCH] Render: a doubled sign is one number, so a cm with it still has a matrix A number written with a doubled sign was read by the renderer's lexer as two numbers. `--280` set the sign, found no digit before the second `-`, and returned 0, leaving `-280` to be read next: a `cm` carrying it got seven operands, kept the last six, and the matrix `0 0 1 0 -280 10` hid everything drawn under it. The page rendered blank while text extraction still reported the text, because the glyphs were there and only the matrix was wrong. Measured on the report's own file, at 72 dpi, counting pixels darker than mid-grey: double_minus.pdf before: 0 dark px (blank) after: 581, leftmost at x=121 control.pdf (-280) 581, leftmost at x=121 -- unchanged so the two files now render identically, which is what the report asks for and what MuPDF does. One extra sign is absorbed and the value is negative if either sign was a minus, matching the COS lexer since #3299 (`--280` and `+-280` are both -280). `Number::skip` already consumed the whole token, so read and skip had disagreed about where the number ended. Fixes #3565. #3299 fixed the same input in the COS lexer only. --- vendor/hayro-syntax/src/object/number.rs | 44 ++++++++++++++++++------ 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/vendor/hayro-syntax/src/object/number.rs b/vendor/hayro-syntax/src/object/number.rs index bd37b19c30..7d3f05b4e1 100644 --- a/vendor/hayro-syntax/src/object/number.rs +++ b/vendor/hayro-syntax/src/object/number.rs @@ -113,17 +113,24 @@ impl Readable<'_> for Number { #[inline(always)] fn read_inner(r: &mut Reader<'_>) -> Option { - let negative = match r.peek_byte()? { - b'-' => { - r.forward(); - true - } - b'+' => { - r.forward(); - false - } - _ => false, - }; + // PdfCraft patch: a doubled sign is one number, not two. `--280` left `negative` set with no + // digits read, so it returned 0 and the reader stopped on `-280`, which became a second + // number: a `cm` with it got seven operands, kept the last six, and the matrix + // `0 0 1 0 -280 10` hid everything drawn under it (#3565). `Number::skip` above already + // consumes the whole token, so read and skip disagreed about where the number ended. + // + // One extra sign is absorbed and the value is negative if either sign was a minus, which is + // what the COS lexer does since #3299 (`--280` and `+-280` are both -280). A third sign is + // left to the digit loop's trailing-minus arm, as before. + let mut negative = false; + let mut signs = 0; + while signs < 2 + && let Some(b @ (b'-' | b'+')) = r.peek_byte() + { + r.forward(); + negative |= b == b'-'; + signs += 1; + } // PdfCraft patch: high-precision float mantissa overflow (jsPDF double-precision coordinates). // Digits that don't fit in `mantissa` are dropped: past the dot they are only precision; @@ -348,6 +355,21 @@ mod tests { use crate::reader::Reader; use crate::reader::ReaderExt; + /// PdfCraft patch: a doubled sign is one number. `--280` returned 0 and left `-280` to be + /// read as a second number, so a `cm` carrying it got seven operands and a degenerate matrix + /// (#3565). The COS lexer reads these the same way since #3299. + #[test] + fn a_doubled_sign_is_one_number() { + for (src, want) in [("--280", -280), ("+-280", -280), ("-+280", -280), ("++280", 280), ("-280", -280), ("+280", 280)] { + let mut r = Reader::new(src.as_bytes()); + assert_eq!(r.read_without_context::().unwrap(), want, "{src}"); + assert!(r.at_end(), "{src}: the whole number was read, leaving nothing for a second one"); + } + // The fraction too, and a lone sign is still zero. + assert_eq!(Reader::new("--280.5".as_bytes()).read_without_context::().unwrap(), -280.5); + assert_eq!(Reader::new("-".as_bytes()).read_without_context::().unwrap(), 0); + } + #[test] fn int_1() { assert_eq!(