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
272 lines (271 loc) • 12.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PdfUtils = void 0;
const pdf_parse_1 = __importDefault(require("pdf-parse"));
const pdf_lib_1 = require("pdf-lib");
const interfaces_1 = require("../interfaces");
const config_1 = require("../config");
class PdfUtils {
/**
* Validates a PDF file and extracts basic information
*/
static async validatePdf(pdfBuffer, fileName, options = {}) {
const { maxFileSize = config_1.PDF_LIMITS.MAX_FILE_SIZE, maxPages = config_1.PDF_LIMITS.MAX_PAGES, allowScanned = false, allowForms = false, minTextLength = config_1.PDF_LIMITS.MIN_TEXT_LENGTH, } = options;
try {
console.log('=== PDF VALIDATION DEBUG ===');
console.log('PDF Buffer length:', pdfBuffer.length);
console.log('Max file size:', maxFileSize);
console.log('File name:', fileName);
console.log('PDF Buffer first 20 bytes:', pdfBuffer.slice(0, 20));
console.log('PDF header check:', pdfBuffer.slice(0, 8).toString());
// Basic file validation
if (pdfBuffer.length > maxFileSize) {
console.log('❌ File too large');
throw new Error(interfaces_1.PdfAccessibilityError.FILE_TOO_LARGE);
}
console.log('✅ File size OK, attempting PDF parse...');
// Parse PDF
const pdfData = await (0, pdf_parse_1.default)(pdfBuffer);
console.log('✅ PDF parse successful');
const pageCount = pdfData.numpages;
const textContent = pdfData.text || '';
const textLength = textContent.length;
const wordCount = textContent.split(/\s+/).filter((word) => word.length > 0).length;
console.log('PDF Data extracted:');
console.log('- Pages:', pageCount);
console.log('- Text length:', textLength);
console.log('- Word count:', wordCount);
console.log('- First 100 chars:', textContent.substring(0, 100));
// Validation checks
console.log('Running validation checks...');
const hasText = textLength >= minTextLength;
const isScanned = textLength < 50; // Heuristic for scanned documents
const tooManyPages = pageCount > maxPages;
console.log('Checking form fields...');
const hasFormFields = this.detectFormFields(pdfBuffer.toString('binary'));
console.log('Checking non-Roman characters...');
const hasNonRomanChars = this.detectNonRomanChars(textContent);
// DEBUG: Show sample of problematic characters
if (hasNonRomanChars) {
const problematicChars = textContent.match(/[^\u0020-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F]/g);
console.log('Problematic characters found:', problematicChars ? problematicChars.slice(0, 10) : 'none');
console.log('Character codes:', problematicChars ? problematicChars.slice(0, 5).map(c => c.charCodeAt(0)) : 'none');
}
console.log('Validation results:');
console.log('- hasText:', hasText, `(${textLength} >= ${minTextLength})`);
console.log('- isScanned:', isScanned);
console.log('- tooManyPages:', tooManyPages, `(${pageCount} > ${maxPages})`);
console.log('- hasFormFields:', hasFormFields);
console.log('- hasNonRomanChars:', hasNonRomanChars);
// Overall validation
const validationDetails = {
fileSize: pdfBuffer.length <= maxFileSize,
pageCount: pageCount <= maxPages,
hasReadableText: hasText,
notScanned: !isScanned || allowScanned,
noForms: !hasFormFields || allowForms,
romanCharsOnly: !hasNonRomanChars,
};
const valid = Object.values(validationDetails).every(check => check);
console.log('Final validation details:', validationDetails);
console.log('Overall valid:', valid);
if (!valid) {
console.log('❌ Validation failed. Failed checks:');
const failedChecks = [];
Object.entries(validationDetails).forEach(([key, value]) => {
if (!value) {
console.log(` - ${key}: FAILED`);
failedChecks.push(key);
}
});
// Create specific error message based on failed checks
let specificError = 'PDF validation failed: ';
if (failedChecks.includes('fileSize')) {
specificError += `File too large (${pdfBuffer.length} bytes > ${maxFileSize} bytes). `;
}
if (failedChecks.includes('pageCount')) {
specificError += `Too many pages (${pageCount} > ${maxPages}). `;
}
if (failedChecks.includes('hasReadableText')) {
specificError += `Insufficient text content (${textLength} chars < ${minTextLength} required). `;
}
if (failedChecks.includes('notScanned')) {
specificError += `Scanned document detected (not allowed). `;
}
if (failedChecks.includes('noForms')) {
specificError += `Form fields detected (not allowed). `;
}
if (failedChecks.includes('romanCharsOnly')) {
specificError += `Non-Roman characters detected. `;
}
return {
valid: false,
pageCount,
textLength,
wordCount,
hasText,
isScanned,
tooManyPages,
hasFormFields,
hasNonRomanChars,
fileSize: pdfBuffer.length,
fileName,
extractedText: textContent.substring(0, config_1.PDF_LIMITS.MAX_TEXT_EXTRACT),
validationDetails,
error: specificError.trim(),
};
}
else {
console.log('✅ All validation checks passed!');
}
return {
valid,
pageCount,
textLength,
wordCount,
hasText,
isScanned,
tooManyPages,
hasFormFields,
hasNonRomanChars,
fileSize: pdfBuffer.length,
fileName,
extractedText: textContent.substring(0, config_1.PDF_LIMITS.MAX_TEXT_EXTRACT),
validationDetails,
};
}
catch (error) {
// Enhanced error handling with specific error messages
console.log('❌ PDF VALIDATION ERROR ===');
console.log('Error type:', typeof error);
console.log('Error instanceof Error:', error instanceof Error);
console.log('Error message:', error instanceof Error ? error.message : String(error));
console.log('Error stack:', error instanceof Error ? error.stack : 'No stack');
let errorMessage = 'Unknown validation error';
if (error instanceof Error) {
errorMessage = error.message;
// Provide specific guidance for common errors
if (error.message.includes('Invalid PDF')) {
errorMessage = 'Invalid PDF format or corrupted file. Please ensure the file is a valid PDF.';
}
else if (error.message.includes('password')) {
errorMessage = 'Password-protected PDFs are not supported. Please provide an unprotected PDF.';
}
else if (error.message.includes('encrypted')) {
errorMessage = 'Encrypted PDFs are not supported. Please provide an unencrypted PDF.';
}
}
console.log('Final error message:', errorMessage);
return {
valid: false,
pageCount: 0,
textLength: 0,
wordCount: 0,
hasText: false,
isScanned: false,
tooManyPages: false,
hasFormFields: false,
hasNonRomanChars: false,
fileSize: pdfBuffer.length,
fileName,
extractedText: '',
validationDetails: {
fileSize: pdfBuffer.length <= maxFileSize,
pageCount: false,
hasReadableText: false,
notScanned: false,
noForms: false,
romanCharsOnly: false,
},
error: errorMessage,
};
}
}
/**
* Applies accessibility improvements to a PDF
*/
static async remediatePdf(pdfBuffer, improvements) {
try {
const pdfDoc = await pdf_lib_1.PDFDocument.load(pdfBuffer);
// Set document title
if (improvements.title) {
pdfDoc.setTitle(improvements.title);
}
// Set language for screen readers
if (improvements.language) {
pdfDoc.setLanguage(improvements.language);
}
// Set accessibility metadata
pdfDoc.setProducer('N8N PDF Accessibility Service v1.0');
pdfDoc.setSubject('WCAG 2.1 AA Compliant Document');
pdfDoc.setKeywords(['accessibility', 'WCAG', 'compliant', 'remediated']);
pdfDoc.setCreator('PDF Accessibility Automation');
pdfDoc.setCreationDate(new Date());
pdfDoc.setModificationDate(new Date());
// Apply additional metadata
if (improvements.metadata) {
Object.entries(improvements.metadata).forEach(([_key, _value]) => {
// Note: pdf-lib has limited metadata support
// For full accessibility tagging, additional libraries would be needed
});
}
// Save the remediated PDF
const remediatedPdfBytes = await pdfDoc.save({
useObjectStreams: false, // Better compatibility
addDefaultPage: false,
objectsPerTick: 50,
});
return Buffer.from(remediatedPdfBytes);
}
catch (error) {
throw new Error(`${config_1.ERROR_MESSAGES.REMEDIATION_FAILED}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Detects if PDF contains form fields
*/
static detectFormFields(pdfRawContent) {
try {
const formIndicators = ['/AcroForm', '/XFA', '/Widget', '/Tx', '/Ch', '/Btn'];
return formIndicators.some(indicator => pdfRawContent.includes(indicator));
}
catch (error) {
// If detection fails, assume no forms to avoid blocking validation
return false;
}
}
/**
* Detects non-Roman characters in text
*/
static detectNonRomanChars(text) {
// Extended check for Latin scripts including Spanish, French, German, etc.
// Allow: Basic Latin, Latin-1 Supplement, Latin Extended-A, Latin Extended-B
// This covers accented characters like á, é, í, ó, ú, ñ, ç, etc.
// Also allow common whitespace characters: space, tab, newline, carriage return
const nonRomanPattern = /[^\u0020-\u007F\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\s]/;
return nonRomanPattern.test(text);
}
/**
* Extracts images and their positions from PDF (placeholder)
*/
static async extractImages(_pdfBuffer) {
// This is a placeholder - full image extraction requires additional libraries
// like pdf2pic or pdf-poppler for complete implementation
return [];
}
/**
* Formats file size for human reading
*/
static formatFileSize(bytes) {
if (bytes === 0)
return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
}
exports.PdfUtils = PdfUtils;