M11: PDF/A verify reports nonzero reals nearer zero than ±1.175 × 10⁻³⁸ (6.1.13)

PDF Association TechNote 0010 A018: PDF/A-2 does not allow the nonzero
reals that ISO 32000-1 Table C.1 lets a reader round to zero.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
mtlcl410
2026-11-05 08:46:57 +08:00
co-authored by Claude Opus 5.5
parent 6ebfada74d
commit bf20038b0b
3 changed files with 23 additions and 8 deletions
+2 -2
View File
@@ -117,7 +117,7 @@ pub const WORDINGS: &[&str] = &[
// The document as a whole.
"The document is encrypted",
"An integer is outside the 32-bit range",
"A real number is outside ±3.403 × 10³⁸",
"A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸",
"A CMap maps a character code to a CID above 65535",
"A string is longer than 32767 bytes",
"A name is longer than 127 bytes",
@@ -528,7 +528,7 @@ pub fn verify(doc: &Document, level: Level) -> Vec<Issue> {
issue("6.1.13", "An integer is outside the 32-bit range", None, None, false);
}
if over.real {
issue("6.1.13", "A real number is outside ±3.403 × 10³⁸", None, None, false);
issue("6.1.13", "A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸", None, None, false);
}
if over.string {
issue("6.1.13", "A string is longer than 32767 bytes", None, None, false);
+6 -2
View File
@@ -6,7 +6,9 @@
//! a name written with `#xx` escapes counts its decoded bytes. A page's `/Contents` streams are read
//! joined, as one content stream (an operator's operands may end one stream and the operator start
//! the next, ISO 32000-1 7.8.2). Content is decoded within one allowance for the whole document.
//! Real numbers beyond ±3.403 × 10³⁸ are reported, and so is an embedded CMap of a Type 0 font
//! Real numbers beyond ±3.403 × 10³⁸ are reported, and so are nonzero ones nearer zero than
//! ±1.175 × 10⁻³⁸ (ISO 32000-1 Table C.1 lets a reader round them to zero; PDF/A-2 does not allow
//! them, PDF Association TechNote 0010 A018), and so is an embedded CMap of a Type 0 font
//! (or one it uses through `/UseCMap`) that maps a code to a CID above 65535. An integer token too
//! long even for a real reads as one past that range, and is reported as a real.
@@ -19,6 +21,8 @@ const MIN_INT: i64 = i32::MIN as i64;
const MAX_INT: i64 = i32::MAX as i64;
/// Table C.1: the largest real value (approximate).
const MAX_REAL: f64 = 3.403e38;
/// Table C.1: the nonzero real value closest to zero (approximate).
const MIN_REAL: f64 = 1.175e-38;
/// Table C.1: the largest character identifier.
const MAX_CID: u64 = 65535;
/// The decoded CMap data, across all the document's embedded CMaps, the scan reads.
@@ -65,7 +69,7 @@ impl Exceeded {
}
match o {
Object::Int(i) => self.int |= !(MIN_INT..=MAX_INT).contains(i),
Object::Real(r) => self.real |= !r.is_finite() || r.abs() > MAX_REAL,
Object::Real(r) => self.real |= !r.is_finite() || r.abs() > MAX_REAL || (*r != 0.0 && r.abs() < MIN_REAL),
Object::String(s) => self.string |= s.bytes.len() > MAX_STRING,
Object::Name(n) => self.name(n),
Object::Array(a) => {
+15 -4
View File
@@ -569,11 +569,15 @@ endstream",
// Corners near ±1e308 are reals past the limit too, and the width overflows to infinity.
assert_eq!(
limits(&page("|", "", "[-1e308 0 1e308 200]", "")),
["A real number is outside ±3.403 × 10³⁸", "Page 1 has a boundary smaller than 3 or larger than 14400 units"]
[
"A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸",
"Page 1 has a boundary smaller than 3 or larger than 14400 units"
]
);
}
/// 6.1.13's two remaining limits of ISO 32000-1 Annex C: a real number beyond ±3.403 × 10³⁸, and
/// 6.1.13's two remaining limits of ISO 32000-1 Annex C: a real number beyond ±3.403 × 10³⁸ (or
/// nonzero and nearer zero than ±1.175 × 10⁻³⁸), and
/// a CID above 65535 that an embedded CMap of a Type 0 font maps to (a `begincidrange` reaching
/// past it, a `begincidchar` naming one, or a CMap it uses through `/UseCMap`).
#[test]
@@ -600,8 +604,15 @@ fn implementation_limits_cover_reals_and_cids() {
let near = format!("34{}.0", "0".repeat(37));
let far = format!("4{}.0", "0".repeat(38));
assert!(limits(&doc(&format!("/R {near} /S -{near}"), &range("0000", "00ff", 65280), "")).is_empty(), "both at their limits");
assert_eq!(limits(&doc(&format!("/R {far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸"]);
assert_eq!(limits(&doc(&format!("/R -{far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸"]);
assert_eq!(limits(&doc(&format!("/R {far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
assert_eq!(limits(&doc(&format!("/R -{far}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
// A nonzero real nearer zero than ±1.175 × 10⁻³⁸ is not allowed either; zero and the
// smallest allowed magnitude are.
let tiny = format!("0.{}1", "0".repeat(39));
let smallest = format!("0.{}1175", "0".repeat(37));
assert!(limits(&doc(&format!("/Z 0.0 /M -0.0 /S {smallest} /T -{smallest}"), "", "")).is_empty(), "zero and the smallest real");
assert_eq!(limits(&doc(&format!("/R {tiny}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
assert_eq!(limits(&doc(&format!("/R -{tiny}"), "", "")), ["A real number is outside ±3.403 × 10³⁸, or nonzero but within ±1.175 × 10⁻³⁸"]);
let cid = ["A CMap maps a character code to a CID above 65535"];
assert_eq!(limits(&doc("", &range("3f00", "3fff", 65536), "")), cid);
assert_eq!(limits(&doc("", &range("0000", "0100", 65280), "")), cid, "the range reaches 65536");