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.
This commit is contained in:
Hamza
2026-10-20 14:47:52 +05:00
parent ec0a965074
commit 766ff9d05a
+33 -11
View File
@@ -113,17 +113,24 @@ impl Readable<'_> for Number {
#[inline(always)]
fn read_inner(r: &mut Reader<'_>) -> Option<Number> {
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::<i32>().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::<f64>().unwrap(), -280.5);
assert_eq!(Reader::new("-".as_bytes()).read_without_context::<i32>().unwrap(), 0);
}
#[test]
fn int_1() {
assert_eq!(