From dafef834d2e4d1058308136fb6a7c42235cee2ac Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Wed, 18 Mar 2026 13:37:19 +0000 Subject: [PATCH] Fix CP14 heading detection via RoleMap + add manual pass support - enterprise_pdf_checker.py: resolve custom tag names through PDF RoleMap in _check_headings so PDFs using /Heading1-style tags (mapped to /H1) are correctly detected; add depth guard to walk_tree - js/results.js: add CP14 (Heading Structure) to CP_TO_CHECK; relax H-type restriction so M-type CPs with a linked check also get Mark as Passed / Undo buttons - api.php: add 'Heading Structure' => ['14'] to $check_to_cp for server-side recalculate score with heading override Co-Authored-By: Claude Sonnet 4.6 --- api.php | 1 + enterprise_pdf_checker.py | 31 +++++++++++++++++++++++++------ js/results.js | 8 ++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/api.php b/api.php index 3dee428..b0c9353 100644 --- a/api.php +++ b/api.php @@ -1372,6 +1372,7 @@ function handleSaveAdjustedResult() { $check_to_cp = [ 'Color Contrast' => ['04'], 'Image Accessibility' => ['13'], + 'Heading Structure' => ['14'], ]; $cp_to_check = []; foreach ($check_to_cp as $checkName => $cpIds) { diff --git a/enterprise_pdf_checker.py b/enterprise_pdf_checker.py index 555bcc9..3d1684a 100644 --- a/enterprise_pdf_checker.py +++ b/enterprise_pdf_checker.py @@ -1311,22 +1311,41 @@ Respond in JSON format: return struct_tree = catalog["/StructTreeRoot"] - headings = [] + if hasattr(struct_tree, 'get_object'): + struct_tree = struct_tree.get_object() - def walk_tree(element): + # Load RoleMap so custom tag names (e.g. /Heading1) resolve to standard ones (/H1) + role_map = {} + if "/RoleMap" in struct_tree: + rm = struct_tree["/RoleMap"] + if hasattr(rm, 'get_object'): + rm = rm.get_object() + try: + for key, value in rm.items(): + role_map[str(key)] = str(value) + except (AttributeError, TypeError): + pass + + headings = [] + HEADING_TAGS = {"/H1", "/H2", "/H3", "/H4", "/H5", "/H6"} + + def walk_tree(element, depth=0): + if depth > 100: + return try: if hasattr(element, 'get_object'): element = element.get_object() if isinstance(element, dict): tag = str(element.get("/S", "")) - if tag in ["/H1", "/H2", "/H3", "/H4", "/H5", "/H6"]: - headings.append(int(tag[2])) + mapped_tag = role_map.get(tag, tag) + if mapped_tag in HEADING_TAGS: + headings.append(int(mapped_tag[2])) kids = element.get("/K", []) if isinstance(kids, list): for kid in kids: - walk_tree(kid) + walk_tree(kid, depth + 1) elif kids: - walk_tree(kids) + walk_tree(kids, depth + 1) except (AttributeError, TypeError, KeyError): pass diff --git a/js/results.js b/js/results.js index 65ffce8..b165208 100644 --- a/js/results.js +++ b/js/results.js @@ -446,7 +446,7 @@ function displayScoreBreakdown(breakdown) { } // Maps H-type Matterhorn checkpoint IDs to the Score Breakdown check names that drive them -const CP_TO_CHECK = { '04': 'Color Contrast', '13': 'Image Accessibility' }; +const CP_TO_CHECK = { '04': 'Color Contrast', '13': 'Image Accessibility', '14': 'Heading Structure' }; function displayMatterhorn(summary) { const card = document.getElementById('matterhornCard'); @@ -459,9 +459,9 @@ function displayMatterhorn(summary) { const cpMap = {}; summary.checkpoints.forEach(cp => { cpMap[cp.id] = cp; }); - // Compute effective status: H-type FAIL → MANUAL_PASS if linked check is overridden + // Compute effective status: FAIL → MANUAL_PASS if linked check is overridden function effectiveStatus(cp) { - if (cp.how === 'H' && cp.status === 'FAIL') { + if (cp.status === 'FAIL') { const linked = CP_TO_CHECK[cp.id]; if (linked && overriddenChecks.has(linked)) return 'MANUAL_PASS'; } @@ -503,7 +503,7 @@ function displayMatterhorn(summary) { `; } else if (effStatus === 'PASS') { statusHtml = `✓ PASS`; - } else if (effStatus === 'FAIL' && cp.how === 'H' && CP_TO_CHECK[cp.id]) { + } else if (effStatus === 'FAIL' && CP_TO_CHECK[cp.id]) { const linked = CP_TO_CHECK[cp.id]; statusHtml = `✗ FAIL `;