From 59efe726075d0110f00dbb5bcb867c807ecbb923 Mon Sep 17 00:00:00 2001 From: DJP Date: Mon, 20 Oct 2025 16:01:52 -0400 Subject: [PATCH] Add Visual Page Inspector with interactive issue markers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend Features: ✨ NEW: Visual Page Inspector component - Display PDF pages as images with zoom controls - SVG overlay system for precise issue highlighting - Color-coded markers by severity (red/orange/yellow/blue) - Numbered badges on each issue for easy reference - Interactive hover tooltips with issue details - Click-through to see exact locations on page User Experience: 📄 Page selector sidebar shows all pages - Color-coded badges indicate issue severity per page - Click any page to view it - Pages with no issues show in green 🔍 Zoom Controls: - Zoom in/out buttons (50% - 300%) - Reset to 100% - Markers scale with zoom level đŸŽ¯ Interactive Markers: - Dashed rectangles highlight issue locations - Hover to see full issue description + fix recommendation - Semi-transparent overlays don't obscure content - Numbered circles for easy cross-reference Backend Support: - API endpoint: api.php?action=image&job_id=X&page=Y - Serves PNG images with proper caching headers - Coordinate system conversion (PDF → screen coords) How It Works: 1. Python generates page images at 100 DPI 2. Issues with coordinates get visual markers 3. SVG overlays drawn at correct positions 4. Tooltips show on hover Perfect for: - Seeing exactly where image/contrast issues are - Visual verification of accessibility problems - Training teams on what to fix - Before/after comparisons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- api.php | 33 ++++++ index.html | 333 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 360 insertions(+), 6 deletions(-) diff --git a/api.php b/api.php index 80fa144..6289ea3 100644 --- a/api.php +++ b/api.php @@ -52,6 +52,9 @@ switch ($action) { case 'debug': handleDebug(); break; + case 'image': + handleImage(); + break; default: error('Invalid action'); } @@ -138,6 +141,7 @@ function handleCheck() { $venv_python = '/Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker/venv/bin/python3'; $python_bin = file_exists($venv_python) ? $venv_python : 'python3'; + // Note: Python script will auto-generate page images when --output is specified $cmd = escapeshellcmd($python_bin . ' ' . PYTHON_SCRIPT) . ' ' . escapeshellarg($pdf_path) . ' ' . '--output ' . escapeshellarg($output_path); @@ -351,6 +355,35 @@ function handleDebug() { success($debug_info); } +/** + * Serve page images + */ +function handleImage() { + $job_id = $_GET['job_id'] ?? ''; + $page_num = $_GET['page'] ?? ''; + + if (empty($job_id) || empty($page_num)) { + error('Job ID and page number required'); + } + + // Find the image file + $images_dir = RESULTS_DIR . '/' . $job_id . '.result_images'; + $image_file = $images_dir . '/page_' . intval($page_num) . '.png'; + + if (!file_exists($image_file)) { + http_response_code(404); + header('Content-Type: application/json'); + echo json_encode(['success' => false, 'error' => 'Image not found']); + exit; + } + + // Serve the image + header('Content-Type: image/png'); + header('Cache-Control: public, max-age=86400'); // Cache for 1 day + readfile($image_file); + exit; +} + /** * Send success response */ diff --git a/index.html b/index.html index be35fbf..8f575da 100644 --- a/index.html +++ b/index.html @@ -616,10 +616,56 @@ - + + + +

Issues & Recommendations

- +
@@ -627,7 +673,7 @@
- +
@@ -922,10 +968,10 @@ // Hide upload, show results document.getElementById('uploadSection').style.display = 'none'; document.getElementById('resultsSection').style.display = 'block'; - + // Display score document.getElementById('scoreNumber').textContent = data.accessibility_score; - + // Display stats const statsGrid = document.getElementById('statsGrid'); statsGrid.innerHTML = ` @@ -950,10 +996,13 @@
Success
`; - + // Store and display issues allIssues = data.issues; displayIssues(allIssues); + + // Initialize visual page viewer if images available + initializePageViewer(data); } function displayIssues(issues) { @@ -1161,10 +1210,282 @@ document.getElementById('uploadSection').style.display = 'block'; document.getElementById('resultsSection').style.display = 'none'; document.getElementById('progressContainer').style.display = 'none'; + document.getElementById('pageViewerCard').style.display = 'none'; document.getElementById('fileInput').value = ''; currentJobId = null; clearLog(); } + + // ==================== VISUAL PAGE VIEWER ==================== + + let currentPageData = null; + let currentZoom = 1.0; + let currentVisualPage = 1; + + function initializePageViewer(data) { + // Check if we have page images + if (!data.page_images || Object.keys(data.page_images).length === 0) { + console.log('No page images available'); + return; + } + + // Show the page viewer card + document.getElementById('pageViewerCard').style.display = 'block'; + + // Store data globally + currentPageData = data; + + // Build page selector + const pageSelector = document.getElementById('pageSelector'); + const pageNumbers = Object.keys(data.page_images).map(Number).sort((a, b) => a - b); + + pageSelector.innerHTML = pageNumbers.map(pageNum => { + const pageIssues = data.issues.filter(issue => issue.page_number === pageNum); + const hasIssues = pageIssues.length > 0; + + let badgeColor = '#10b981'; // Green + if (pageIssues.some(i => i.severity === 'CRITICAL')) { + badgeColor = '#dc2626'; + } else if (pageIssues.some(i => i.severity === 'ERROR')) { + badgeColor = '#ef4444'; + } else if (pageIssues.some(i => i.severity === 'WARNING')) { + badgeColor = '#f59e0b'; + } + + return ` + + `; + }).join(''); + + // Load first page with issues, or first page + const firstPageWithIssues = pageNumbers.find(p => data.issues.some(i => i.page_number === p)); + loadVisualPage(firstPageWithIssues || pageNumbers[0]); + } + + function loadVisualPage(pageNum) { + if (!currentPageData || !currentPageData.page_images[pageNum]) { + console.error('Page not found:', pageNum); + return; + } + + currentVisualPage = pageNum; + + // Update title + document.getElementById('currentPageTitle').textContent = `Page ${pageNum}`; + + // Highlight selected page button + document.querySelectorAll('[id^="pageBtn"]').forEach(btn => { + btn.style.background = 'white'; + btn.style.fontWeight = 'normal'; + }); + const selectedBtn = document.getElementById(`pageBtn${pageNum}`); + if (selectedBtn) { + selectedBtn.style.background = '#f0f9ff'; + selectedBtn.style.fontWeight = '600'; + } + + // Load page image + const imageUrl = `api.php?action=image&job_id=${currentJobId}&page=${pageNum}`; + const pageImage = document.getElementById('pageImage'); + + pageImage.onload = function() { + // Draw markers for issues on this page + drawMarkers(pageNum); + }; + + pageImage.src = imageUrl; + } + + function drawMarkers(pageNum) { + const svg = document.getElementById('markerOverlay'); + const pageImage = document.getElementById('pageImage'); + + // Clear existing markers + svg.innerHTML = ''; + + // Get image dimensions + const imgWidth = pageImage.naturalWidth; + const imgHeight = pageImage.naturalHeight; + const displayWidth = pageImage.clientWidth; + const displayHeight = pageImage.clientHeight; + + // Set SVG viewBox to match image natural size + svg.setAttribute('viewBox', `0 0 ${imgWidth} ${imgHeight}`); + svg.setAttribute('width', displayWidth); + svg.setAttribute('height', displayHeight); + + // Get issues for this page + const pageIssues = currentPageData.issues.filter(issue => + issue.page_number === pageNum && issue.coordinates + ); + + if (pageIssues.length === 0) { + console.log(`No issues with coordinates on page ${pageNum}`); + return; + } + + // Draw markers for each issue + pageIssues.forEach((issue, index) => { + const coords = issue.coordinates; + + // PDF coordinates are bottom-left origin, need to flip Y + const x0 = coords.x0; + const y0 = imgHeight - coords.y1; // Flip Y + const x1 = coords.x1; + const y1 = imgHeight - coords.y0; // Flip Y + + const width = x1 - x0; + const height = y1 - y0; + + // Color based on severity + let strokeColor, fillColor; + switch (issue.severity) { + case 'CRITICAL': + strokeColor = '#dc2626'; + fillColor = 'rgba(220, 38, 38, 0.2)'; + break; + case 'ERROR': + strokeColor = '#ef4444'; + fillColor = 'rgba(239, 68, 68, 0.2)'; + break; + case 'WARNING': + strokeColor = '#f59e0b'; + fillColor = 'rgba(245, 158, 11, 0.2)'; + break; + default: + strokeColor = '#3b82f6'; + fillColor = 'rgba(59, 130, 246, 0.2)'; + } + + // Create rectangle + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + rect.setAttribute('x', x0); + rect.setAttribute('y', y0); + rect.setAttribute('width', width); + rect.setAttribute('height', height); + rect.setAttribute('fill', fillColor); + rect.setAttribute('stroke', strokeColor); + rect.setAttribute('stroke-width', '3'); + rect.setAttribute('stroke-dasharray', '5,5'); + rect.setAttribute('rx', '4'); + rect.style.cursor = 'pointer'; + rect.style.pointerEvents = 'all'; + + // Add tooltip on hover + rect.addEventListener('mouseenter', function(e) { + showIssueTooltip(e, issue); + }); + + rect.addEventListener('mouseleave', function() { + hideIssueTooltip(); + }); + + svg.appendChild(rect); + + // Add number badge + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); + circle.setAttribute('cx', x0 + 15); + circle.setAttribute('cy', y0 + 15); + circle.setAttribute('r', '12'); + circle.setAttribute('fill', strokeColor); + svg.appendChild(circle); + + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', x0 + 15); + text.setAttribute('y', y0 + 20); + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('fill', 'white'); + text.setAttribute('font-size', '12'); + text.setAttribute('font-weight', 'bold'); + text.textContent = index + 1; + svg.appendChild(text); + }); + + console.log(`Drew ${pageIssues.length} markers on page ${pageNum}`); + } + + let tooltipDiv = null; + + function showIssueTooltip(event, issue) { + if (!tooltipDiv) { + tooltipDiv = document.createElement('div'); + tooltipDiv.style.position = 'fixed'; + tooltipDiv.style.background = 'rgba(0, 0, 0, 0.9)'; + tooltipDiv.style.color = 'white'; + tooltipDiv.style.padding = '12px'; + tooltipDiv.style.borderRadius = '8px'; + tooltipDiv.style.maxWidth = '300px'; + tooltipDiv.style.zIndex = '10000'; + tooltipDiv.style.fontSize = '14px'; + tooltipDiv.style.pointerEvents = 'none'; + document.body.appendChild(tooltipDiv); + } + + tooltipDiv.innerHTML = ` +
+ ${issue.severity}: ${issue.category} +
+
${issue.description}
+ ${issue.recommendation ? `
+ Fix: ${issue.recommendation} +
` : ''} + `; + + tooltipDiv.style.display = 'block'; + tooltipDiv.style.left = (event.clientX + 15) + 'px'; + tooltipDiv.style.top = (event.clientY + 15) + 'px'; + } + + function hideIssueTooltip() { + if (tooltipDiv) { + tooltipDiv.style.display = 'none'; + } + } + + function getSeverityColor(severity) { + switch (severity) { + case 'CRITICAL': return '#dc2626'; + case 'ERROR': return '#ef4444'; + case 'WARNING': return '#f59e0b'; + default: return '#3b82f6'; + } + } + + function zoomIn() { + currentZoom = Math.min(currentZoom + 0.25, 3.0); + applyZoom(); + } + + function zoomOut() { + currentZoom = Math.max(currentZoom - 0.25, 0.5); + applyZoom(); + } + + function resetZoom() { + currentZoom = 1.0; + applyZoom(); + } + + function applyZoom() { + const pageImage = document.getElementById('pageImage'); + pageImage.style.transform = `scale(${currentZoom})`; + pageImage.style.transformOrigin = 'top left'; + document.getElementById('zoomLevel').textContent = `${Math.round(currentZoom * 100)}%`; + + // Redraw markers at new scale + if (currentVisualPage) { + setTimeout(() => drawMarkers(currentVisualPage), 50); + } + }