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 @@
-
+
+
+
+
đ Visual Page Inspector
+
Click on issues below to see their exact location on the page
+
+
+
+
+
+
+
+
+
Page 1
+
+
+ 100%
+
+
+
+
+
+
+
+
![PDF Page]()
+
+
+
+
+
+ Legend:
+ đ¨ Critical
+ â Error
+ â ī¸ Warning
+ âšī¸ Info
+
+
+
+
+
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);
+ }
+ }