UNPKG

n8n-nodes-pdf-accessibility

Version:

AI-powered PDF accessibility automation for N8N - comprehensive WCAG compliance analysis, intelligent remediation, and professional audit reporting with 5 integrated accessibility tools

216 lines (215 loc) 9.61 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ImageAltTextTool = void 0; class ImageAltTextTool { getName() { return 'image_alttext'; } getDescription() { return 'Generates AI-powered alt-text descriptions for images in PDF documents'; } getSupportedWCAGCriteria() { return [ '1.1.1', // Non-text Content (Level A) '1.4.5', // Images of Text (Level AA) '1.4.9', // Images of Text (No Exception) (Level AAA) ]; } canProcess(context) { return context.hasImages; } async execute(context, llmProvider) { const startTime = Date.now(); const issues = []; const fixes = []; try { // Extract image information from PDF const images = await this.extractImageInfo(context); if (images.length === 0) { return { toolName: this.getName(), success: true, issuesFound: [], fixesApplied: [], processing_time_ms: Date.now() - startTime, }; } // Analyze each image for accessibility issues for (const image of images) { const imageIssues = await this.analyzeImage(image, context); issues.push(...imageIssues); // Generate alt-text if needed and LLM provider is available if (llmProvider && this.needsAltText(image)) { try { const altTextFix = await this.generateAltText(image, context, llmProvider); fixes.push(altTextFix); } catch (error) { // Alt-text generation failed, but continue with other images console.warn(`Alt-text generation failed for image ${image.index}:`, error); } } } return { toolName: this.getName(), success: true, issuesFound: issues, fixesApplied: fixes, processing_time_ms: Date.now() - startTime, }; } catch (error) { return { toolName: this.getName(), success: false, issuesFound: issues, fixesApplied: fixes, processing_time_ms: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } async extractImageInfo(context) { // This is a placeholder implementation // In a real implementation, you would use pdf-lib or similar to extract actual image data const images = []; // Simulate image detection based on content analysis const imagePatterns = [ /figure\s*\d+/gi, /chart\s*\d+/gi, /diagram/gi, /illustration/gi, /photo/gi, /image/gi, ]; let imageCount = 0; imagePatterns.forEach(pattern => { const matches = context.textContent.match(pattern); if (matches) { imageCount += matches.length; } }); // Create mock image info for detected images for (let i = 0; i < Math.min(imageCount, 10); i++) { images.push({ index: i, page: Math.floor(i / 3) + 1, // Distribute across pages width: 400 + Math.random() * 200, height: 300 + Math.random() * 150, position: { x: 100 + Math.random() * 300, y: 100 + Math.random() * 500 }, contextText: this.extractContextText(context.textContent, i), existingAltText: undefined, // Assume no existing alt-text isPrimaryContent: Math.random() > 0.3, // 70% are primary content isDecorative: Math.random() > 0.8, // 20% are decorative }); } return images; } extractContextText(fullText, imageIndex) { // Extract surrounding text for context const words = fullText.split(/\s+/); const position = Math.floor((imageIndex + 1) * words.length / 10); const start = Math.max(0, position - 20); const end = Math.min(words.length, position + 20); return words.slice(start, end).join(' '); } async analyzeImage(image, _context) { const issues = []; // Check for missing alt-text if (!image.existingAltText && !image.isDecorative) { const severity = image.isPrimaryContent ? 'high' : 'medium'; issues.push({ type: 'missing_alt_text', severity, description: `Image ${image.index + 1} on page ${image.page} lacks alt-text description`, location: `Page ${image.page}, Position (${Math.round(image.position.x)}, ${Math.round(image.position.y)})`, wcagCriteria: ['1.1.1'], suggestion: image.isPrimaryContent ? 'Add descriptive alt-text explaining the content and purpose of this image' : 'Add brief alt-text or mark as decorative if purely aesthetic', }); } // Check for potentially decorative images with alt-text if (image.existingAltText && image.isDecorative) { issues.push({ type: 'missing_alt_text', severity: 'low', description: `Image ${image.index + 1} appears decorative but has alt-text`, location: `Page ${image.page}`, wcagCriteria: ['1.1.1'], suggestion: 'Consider marking decorative images with empty alt-text (alt="")', }); } // Check for complex images needing longer descriptions if (image.isPrimaryContent && this.isComplexImage(image)) { issues.push({ type: 'missing_alt_text', severity: 'medium', description: `Complex image ${image.index + 1} may need extended description`, location: `Page ${image.page}`, wcagCriteria: ['1.1.1'], suggestion: 'Complex images like charts or diagrams may need detailed descriptions beyond alt-text', }); } return issues; } isComplexImage(image) { // Heuristics to determine if image is complex const area = image.width * image.height; const aspectRatio = image.width / image.height; // Large images or unusual aspect ratios might be charts/diagrams return area > 100000 || aspectRatio > 3 || aspectRatio < 0.33; } needsAltText(image) { return !image.existingAltText && !image.isDecorative && image.isPrimaryContent; } async generateAltText(image, context, _llmProvider) { // const prompt = this.buildAltTextPrompt(image, context); // Future: Use prompt with LLM provider for real alt-text generation try { // This would use the LLM provider to generate alt-text // For now, we'll create a mock implementation const altText = await this.mockGenerateAltText(image, context); return { type: 'alt_text_generation', description: `Generated alt-text for image ${image.index + 1}`, applied: false, // Would be true if actually applied to PDF wcagImprovement: ['1.1.1'], beforeValue: image.existingAltText || 'No alt-text', afterValue: altText, }; } catch (error) { throw new Error(`Failed to generate alt-text: ${error instanceof Error ? error.message : String(error)}`); } } async mockGenerateAltText(image, _context) { // Mock implementation - in real use, this would call the LLM const contextLower = (image.contextText || '').toLowerCase(); if (contextLower.includes('chart') || contextLower.includes('graph')) { return `Chart showing data visualization on page ${image.page}`; } else if (contextLower.includes('diagram') || contextLower.includes('flow')) { return `Diagram illustrating process or concept described in surrounding text`; } else if (contextLower.includes('photo') || contextLower.includes('picture')) { return `Photograph related to the content on page ${image.page}`; } else if (contextLower.includes('logo') || contextLower.includes('brand')) { return `Company or organization logo`; } else { return `Illustration supporting the content about ${this.extractKeywords(image.contextText || '')}`; } } extractKeywords(text) { // Simple keyword extraction for mock alt-text const words = text.toLowerCase().split(/\s+/); const commonWords = new Set(['the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should']); const keywords = words .filter(word => word.length > 3 && !commonWords.has(word)) .slice(0, 3); return keywords.length > 0 ? keywords.join(', ') : 'document content'; } } exports.ImageAltTextTool = ImageAltTextTool;