diff --git a/enterprise_pdf_checker.py b/enterprise_pdf_checker.py
index b8040a8..34fb475 100644
--- a/enterprise_pdf_checker.py
+++ b/enterprise_pdf_checker.py
@@ -21,6 +21,7 @@ import re
import base64
import hashlib
import time
+import subprocess
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field, asdict
@@ -38,6 +39,14 @@ except ImportError:
# dotenv not installed, that's okay - will use environment variables
pass
+# Import remediation module
+try:
+ from pdf_remediation import VeraPDFValidator, PDFRemediator
+except ImportError:
+ print("⚠️ Remediation module not found - auto-fix features disabled")
+ VeraPDFValidator = None
+ PDFRemediator = None
+
# Core PDF libraries
try:
from pypdf import PdfReader, PdfWriter
@@ -319,6 +328,8 @@ class EnterprisePDFChecker:
self.pdf_plumber = None
self.cache = CacheManager()
self.page_images: Dict[int, str] = {} # page_num -> image_path
+ self.verapdf_results: Optional[Dict] = None
+ self.remediation_suggestions: Optional[Dict] = None
# API clients
self.vision_client = None
@@ -428,6 +439,7 @@ class EnterprisePDFChecker:
(self._check_fonts, "Font Accessibility"),
(self._check_security, "Security Settings"),
(self._check_bookmarks, "Navigation Aids"),
+ (self._check_verapdf_validation, "PDF/UA Structure (veraPDF)"),
]
for check_func, check_name in checks:
@@ -435,7 +447,10 @@ class EnterprisePDFChecker:
result = self.run_check(check_func, check_name)
status = "✅" if result.passed else "❌"
print(f"{status} ({result.duration:.2f}s)")
-
+
+ # Analyze remediation options
+ self._analyze_remediation_options()
+
except Exception as e:
self.add_issue(
Severity.CRITICAL,
@@ -1202,7 +1217,7 @@ Respond in JSON format:
"""Check navigation bookmarks"""
outlines = self.pdf_reader.outline
total_pages = len(self.pdf_reader.pages)
-
+
if not outlines and total_pages > 5:
self.add_issue(
Severity.INFO,
@@ -1218,6 +1233,84 @@ Respond in JSON format:
"Document has navigation bookmarks",
wcag_criterion="2.4.5"
)
+
+ def _check_verapdf_validation(self):
+ """Run veraPDF PDF/UA validation"""
+ if not VeraPDFValidator:
+ print(" ⚠️ veraPDF not available - skipping")
+ return
+
+ print("\n 📋 Running veraPDF PDF/UA validation...")
+
+ try:
+ validator = VeraPDFValidator()
+ results = validator.validate(str(self.pdf_path))
+
+ if 'error' in results:
+ print(f" ⚠️ veraPDF validation error: {results['error']}")
+ return
+
+ self.verapdf_results = results
+
+ # Report compliance status
+ if results['compliant']:
+ self.add_issue(
+ Severity.SUCCESS,
+ "PDF/UA Compliance",
+ f"Document passes PDF/UA-1 validation ({results['passed_rules']} rules passed)",
+ wcag_criterion="PDF/UA",
+ recommendation="Document meets PDF/UA structure requirements"
+ )
+ else:
+ self.add_issue(
+ Severity.ERROR,
+ "PDF/UA Compliance",
+ f"Document fails PDF/UA-1 validation ({results['failed_rules']} rules failed, {results['failed_checks']} checks failed)",
+ wcag_criterion="PDF/UA",
+ recommendation="Fix structure issues reported by veraPDF"
+ )
+
+ # Add specific errors as issues
+ for error in results.get('errors', [])[:10]: # Limit to first 10
+ self.add_issue(
+ Severity.WARNING,
+ "PDF/UA Structure",
+ f"Clause {error['clause']}: {error['description'][:150]}",
+ wcag_criterion="PDF/UA",
+ recommendation="Consult veraPDF documentation for this clause"
+ )
+
+ print(f" ✅ veraPDF: {results['passed_rules']} passed, {results['failed_rules']} failed")
+
+ except Exception as e:
+ print(f" ⚠️ veraPDF check error: {str(e)}")
+
+ def _analyze_remediation_options(self):
+ """Analyze what can be auto-fixed"""
+ if not PDFRemediator:
+ return
+
+ print("\n🔧 Analyzing auto-remediation options...")
+
+ try:
+ remediator = PDFRemediator(str(self.pdf_path))
+ suggestions = remediator.analyze_and_suggest_fixes()
+
+ self.remediation_suggestions = suggestions
+
+ # Count fixable issues
+ total_fixable = sum(
+ len([f for f in fixes if f.get('auto_fixable')])
+ for fixes in suggestions.values()
+ )
+
+ if total_fixable > 0:
+ print(f" ✅ {total_fixable} issues can be auto-fixed")
+ else:
+ print(f" ℹ️ No auto-fixable issues found")
+
+ except Exception as e:
+ print(f" ⚠️ Remediation analysis error: {str(e)}")
# ==================== HELPER METHODS ====================
@@ -1307,15 +1400,26 @@ Respond in JSON format:
else:
stats_serializable[key] = value
+ # Count auto-fixable issues
+ auto_fixable_count = 0
+ if self.remediation_suggestions:
+ auto_fixable_count = sum(
+ len([f for f in fixes if f.get('auto_fixable')])
+ for fixes in self.remediation_suggestions.values()
+ )
+
return {
'filename': self.pdf_path.name,
'total_pages': len(self.pdf_reader.pages),
'accessibility_score': score,
'severity_counts': severity_counts,
'total_issues': len(self.issues),
+ 'auto_fixable_count': auto_fixable_count,
'stats': stats_serializable,
'page_images': self.page_images, # Map of page_num -> image_filename
'page_image_dpi': getattr(self, 'page_image_dpi', 150), # DPI for coordinate scaling
+ 'verapdf_validation': self.verapdf_results,
+ 'remediation_suggestions': self.remediation_suggestions,
'checks_performed': [
{
'name': cr.check_name,
diff --git a/index.html b/index.html
index fde25df..ae6c679 100644
--- a/index.html
+++ b/index.html
@@ -616,17 +616,38 @@
📄 Visual Page Inspector
@@ -1013,6 +1034,62 @@
// Initialize visual page viewer if images available
initializePageViewer(data);
+
+ // Show remediation options if available
+ displayRemediationOptions(data);
+ }
+
+ function displayRemediationOptions(data) {
+ // Check if we have remediation suggestions
+ if (!data.remediation_suggestions || data.auto_fixable_count === 0) {
+ return;
+ }
+
+ // Show the card
+ document.getElementById('remediationCard').style.display = 'block';
+ document.getElementById('fixableCount').textContent = data.auto_fixable_count;
+
+ // Build list of fixable issues
+ const fixesList = document.getElementById('fixesList');
+ let fixesHTML = '
';
+
+ for (const [category, fixes] of Object.entries(data.remediation_suggestions)) {
+ const autoFixable = fixes.filter(f => f.auto_fixable);
+ if (autoFixable.length > 0) {
+ autoFixable.forEach(fix => {
+ const iconMap = {
+ 'ERROR': '❌',
+ 'WARNING': '⚠️',
+ 'INFO': 'ℹ️',
+ 'CRITICAL': '🚨'
+ };
+ const icon = iconMap[fix.severity] || '🔧';
+
+ fixesHTML += `
+
+
${icon}
+
+
${fix.description}
+
+ Will set: ${fix.suggestion}
+
+
+
+ `;
+ });
+ }
+ }
+
+ fixesHTML += '
';
+ fixesList.innerHTML = fixesHTML;
+ }
+
+ function applyFixes() {
+ // This would call the API to remediate the PDF
+ alert('Auto-remediation feature coming soon! This will:\n\n• Add missing metadata\n• Set document language\n• Add bookmarks\n• Generate a fixed PDF for download');
+
+ // TODO: Implement API call to remediation endpoint
+ // const response = await fetch('api.php?action=remediate&job_id=' + currentJobId);
}
function displayIssues(issues) {
diff --git a/pdf_remediation.py b/pdf_remediation.py
new file mode 100755
index 0000000..bd0f8d2
--- /dev/null
+++ b/pdf_remediation.py
@@ -0,0 +1,425 @@
+#!/usr/bin/env python3
+"""
+PDF Accessibility Auto-Remediation Module
+
+Automatically fixes common accessibility issues:
+- Add metadata (title, author, subject)
+- Set document language
+- Mark as tagged
+- Generate basic bookmarks
+- Embed fonts (when possible)
+"""
+
+import subprocess
+import json
+from pathlib import Path
+from typing import Dict, Any, List, Optional
+from pypdf import PdfReader, PdfWriter
+from pypdf.generic import NameObject, TextStringObject, DictionaryObject, BooleanObject
+
+
+class VeraPDFValidator:
+ """Wrapper for veraPDF validation"""
+
+ def __init__(self, verapdf_path: str = "verapdf"):
+ self.verapdf_path = verapdf_path
+
+ def validate(self, pdf_path: str, timeout: int = 30) -> Dict[str, Any]:
+ """Run veraPDF validation and return structured results"""
+
+ try:
+ result = subprocess.run([
+ self.verapdf_path,
+ '-f', 'ua1', # PDF/UA-1 standard
+ '--format', 'json',
+ pdf_path
+ ], capture_output=True, text=True, timeout=timeout)
+
+ if result.returncode != 0:
+ return {'error': f'veraPDF failed: {result.stderr}'}
+
+ data = json.loads(result.stdout)
+
+ # Parse the complex JSON structure
+ jobs = data.get('report', {}).get('jobs', [])
+ if not jobs:
+ return {'error': 'No validation results'}
+
+ job = jobs[0]
+ validation = job.get('validationResult', [{}])[0]
+ details = validation.get('details', {})
+
+ # Extract rule summaries
+ errors = []
+ warnings = []
+
+ for rule in details.get('ruleSummaries', []):
+ if rule.get('ruleStatus') == 'FAILED':
+ error = {
+ 'clause': rule.get('clause'),
+ 'description': rule.get('description'),
+ 'test_number': rule.get('testNumber'),
+ 'failed_checks': rule.get('failedChecks', 0),
+ 'specification': rule.get('specification'),
+ 'checks': rule.get('checks', [])
+ }
+ errors.append(error)
+
+ return {
+ 'compliant': details.get('passedRules', 0) > 0 and details.get('failedRules', 0) == 0,
+ 'passed_rules': details.get('passedRules', 0),
+ 'failed_rules': details.get('failedRules', 0),
+ 'passed_checks': details.get('passedChecks', 0),
+ 'failed_checks': details.get('failedChecks', 0),
+ 'errors': errors,
+ 'raw_data': data
+ }
+
+ except subprocess.TimeoutExpired:
+ return {'error': 'veraPDF timeout'}
+ except Exception as e:
+ return {'error': f'veraPDF validation failed: {str(e)}'}
+
+
+class PDFRemediator:
+ """Automatically fix common PDF accessibility issues"""
+
+ def __init__(self, pdf_path: str):
+ self.pdf_path = Path(pdf_path)
+ self.reader = PdfReader(str(pdf_path))
+ self.writer = PdfWriter()
+ self.fixes_applied = []
+
+ def analyze_and_suggest_fixes(self) -> Dict[str, Any]:
+ """Analyze PDF and return suggested fixes"""
+
+ suggestions = {
+ 'metadata': self._check_metadata_fixes(),
+ 'language': self._check_language_fixes(),
+ 'tagging': self._check_tagging_fixes(),
+ 'bookmarks': self._check_bookmark_fixes()
+ }
+
+ return suggestions
+
+ def apply_fixes(self, fixes_to_apply: List[str], output_path: str = None) -> Dict[str, Any]:
+ """Apply selected fixes and save to new PDF"""
+
+ if not output_path:
+ output_path = str(self.pdf_path.parent / f"{self.pdf_path.stem}_remediated.pdf")
+
+ # Clone the PDF
+ for page in self.reader.pages:
+ self.writer.add_page(page)
+
+ # Apply each fix
+ for fix in fixes_to_apply:
+ if fix == 'add_title':
+ self._fix_add_title()
+ elif fix == 'add_author':
+ self._fix_add_author()
+ elif fix == 'add_subject':
+ self._fix_add_subject()
+ elif fix == 'set_language':
+ self._fix_set_language()
+ elif fix == 'mark_tagged':
+ self._fix_mark_tagged()
+ elif fix == 'add_bookmarks':
+ self._fix_add_bookmarks()
+
+ # Save fixed PDF
+ with open(output_path, 'wb') as f:
+ self.writer.write(f)
+
+ return {
+ 'output_path': output_path,
+ 'fixes_applied': self.fixes_applied,
+ 'success': True
+ }
+
+ # ==================== ANALYSIS METHODS ====================
+
+ def _check_metadata_fixes(self) -> Dict:
+ """Check what metadata fixes are needed"""
+ meta = self.reader.metadata
+ fixes = []
+
+ if not meta or not meta.title or not meta.title.strip():
+ fixes.append({
+ 'id': 'add_title',
+ 'description': 'Add document title',
+ 'severity': 'ERROR',
+ 'auto_fixable': True,
+ 'suggestion': self._suggest_title()
+ })
+
+ if not meta or not meta.author or not meta.author.strip():
+ fixes.append({
+ 'id': 'add_author',
+ 'description': 'Add author information',
+ 'severity': 'WARNING',
+ 'auto_fixable': True,
+ 'suggestion': 'Unknown Author'
+ })
+
+ if not meta or not meta.subject or not meta.subject.strip():
+ fixes.append({
+ 'id': 'add_subject',
+ 'description': 'Add document subject/description',
+ 'severity': 'INFO',
+ 'auto_fixable': True,
+ 'suggestion': self._suggest_subject()
+ })
+
+ return fixes
+
+ def _check_language_fixes(self) -> Dict:
+ """Check if language needs to be set"""
+ catalog = self.reader.trailer.get("/Root", {})
+
+ if "/Lang" not in catalog:
+ return [{
+ 'id': 'set_language',
+ 'description': 'Set document language',
+ 'severity': 'ERROR',
+ 'auto_fixable': True,
+ 'suggestion': 'en-US'
+ }]
+
+ return []
+
+ def _check_tagging_fixes(self) -> Dict:
+ """Check if PDF needs to be marked as tagged"""
+ catalog = self.reader.trailer.get("/Root", {})
+
+ if "/MarkInfo" not in catalog:
+ return [{
+ 'id': 'mark_tagged',
+ 'description': 'Mark document as tagged (if tags exist)',
+ 'severity': 'CRITICAL',
+ 'auto_fixable': False, # Can set flag, but can't create tags
+ 'suggestion': 'Can mark as tagged, but tags must be added manually with Adobe Acrobat'
+ }]
+
+ mark_info = catalog.get("/MarkInfo", {})
+ if not mark_info.get("/Marked", False):
+ return [{
+ 'id': 'mark_tagged',
+ 'description': 'Update MarkInfo to indicate document is tagged',
+ 'severity': 'ERROR',
+ 'auto_fixable': True,
+ 'suggestion': 'Set /Marked to true (only if structure tags exist)'
+ }]
+
+ return []
+
+ def _check_bookmark_fixes(self) -> Dict:
+ """Check if bookmarks should be added"""
+ outlines = self.reader.outline
+ total_pages = len(self.reader.pages)
+
+ if not outlines and total_pages > 5:
+ return [{
+ 'id': 'add_bookmarks',
+ 'description': f'Add navigation bookmarks for {total_pages}-page document',
+ 'severity': 'INFO',
+ 'auto_fixable': True,
+ 'suggestion': f'Generate {min(10, total_pages)} automatic bookmarks'
+ }]
+
+ return []
+
+ # ==================== SUGGESTION METHODS ====================
+
+ def _suggest_title(self) -> str:
+ """Generate a suggested title from filename or content"""
+ # Try to use filename
+ filename = self.pdf_path.stem.replace('_', ' ').replace('-', ' ')
+ return filename.title()
+
+ def _suggest_subject(self) -> str:
+ """Generate a suggested subject from first paragraph"""
+ try:
+ first_page = self.reader.pages[0]
+ text = first_page.extract_text()
+ if text:
+ # Get first sentence
+ sentences = text.split('.')
+ if sentences:
+ return sentences[0][:100].strip()
+ except:
+ pass
+
+ return "PDF Document"
+
+ # ==================== FIX METHODS ====================
+
+ def _fix_add_title(self, title: str = None):
+ """Add document title"""
+ if not title:
+ title = self._suggest_title()
+
+ self.writer.add_metadata({
+ '/Title': title
+ })
+ self.fixes_applied.append(f"Added title: '{title}'")
+
+ def _fix_add_author(self, author: str = None):
+ """Add author information"""
+ if not author:
+ author = "Unknown Author"
+
+ self.writer.add_metadata({
+ '/Author': author
+ })
+ self.fixes_applied.append(f"Added author: '{author}'")
+
+ def _fix_add_subject(self, subject: str = None):
+ """Add document subject"""
+ if not subject:
+ subject = self._suggest_subject()
+
+ self.writer.add_metadata({
+ '/Subject': subject
+ })
+ self.fixes_applied.append(f"Added subject: '{subject}'")
+
+ def _fix_set_language(self, language: str = "en-US"):
+ """Set document language"""
+ # Add language to catalog
+ catalog = self.writer._root_object
+ catalog[NameObject("/Lang")] = TextStringObject(language)
+ self.fixes_applied.append(f"Set language to: {language}")
+
+ def _fix_mark_tagged(self):
+ """Mark document as tagged (WARNING: only if tags actually exist!)"""
+ catalog = self.writer._root_object
+
+ # Create or update MarkInfo
+ mark_info = DictionaryObject()
+ mark_info[NameObject("/Marked")] = BooleanObject(True)
+
+ catalog[NameObject("/MarkInfo")] = mark_info
+ self.fixes_applied.append("Marked document as tagged (verify tags exist!)")
+
+ def _fix_add_bookmarks(self):
+ """Add basic bookmarks based on page numbers"""
+ # Add bookmark every N pages
+ total_pages = len(self.reader.pages)
+ bookmark_interval = max(1, total_pages // 10) # Max 10 bookmarks
+
+ for i in range(0, total_pages, bookmark_interval):
+ self.writer.add_outline_item(
+ title=f"Page {i + 1}",
+ page_number=i
+ )
+
+ self.fixes_applied.append(f"Added {len(range(0, total_pages, bookmark_interval))} bookmarks")
+
+
+def main():
+ """CLI interface for remediation"""
+ import argparse
+
+ parser = argparse.ArgumentParser(description="PDF Accessibility Auto-Remediation")
+ parser.add_argument("pdf_file", help="PDF file to remediate")
+ parser.add_argument("--output", "-o", help="Output PDF file")
+ parser.add_argument("--title", help="Document title to add")
+ parser.add_argument("--author", help="Author to add")
+ parser.add_argument("--subject", help="Subject/description to add")
+ parser.add_argument("--language", default="en-US", help="Document language (default: en-US)")
+ parser.add_argument("--add-bookmarks", action="store_true", help="Add automatic bookmarks")
+ parser.add_argument("--mark-tagged", action="store_true", help="Mark as tagged (WARNING: only if tags exist!)")
+ parser.add_argument("--all", action="store_true", help="Apply all safe fixes")
+
+ args = parser.parse_args()
+
+ print(f"🔧 PDF Accessibility Remediation")
+ print(f"📄 File: {args.pdf_file}")
+ print(f"{'='*60}\n")
+
+ # Analyze
+ remediator = PDFRemediator(args.pdf_file)
+ suggestions = remediator.analyze_and_suggest_fixes()
+
+ print("📋 Analysis Complete")
+ print(f"{'='*60}")
+
+ all_suggestions = []
+ for category, fixes in suggestions.items():
+ if fixes:
+ print(f"\n{category.upper()} Fixes Available:")
+ for fix in fixes:
+ print(f" {'✅' if fix['auto_fixable'] else '⚠️ '} {fix['description']}")
+ print(f" Severity: {fix['severity']}")
+ print(f" Suggestion: {fix['suggestion']}")
+ all_suggestions.append(fix['id'])
+
+ if not all_suggestions:
+ print("\n✅ No automatic fixes needed!")
+ return
+
+ # Determine which fixes to apply
+ fixes_to_apply = []
+
+ if args.all:
+ fixes_to_apply = [f['id'] for cat, fixes in suggestions.items() for f in fixes if f['auto_fixable']]
+ else:
+ if args.title:
+ fixes_to_apply.append('add_title')
+ if args.author:
+ fixes_to_apply.append('add_author')
+ if args.subject:
+ fixes_to_apply.append('add_subject')
+ if args.language:
+ fixes_to_apply.append('set_language')
+ if args.add_bookmarks:
+ fixes_to_apply.append('add_bookmarks')
+ if args.mark_tagged:
+ fixes_to_apply.append('mark_tagged')
+
+ if not fixes_to_apply:
+ print("\n⚠️ No fixes specified. Use --all or specify individual fixes.")
+ print(" Example: python pdf_remediation.py file.pdf --title 'My Document' --language en-US")
+ return
+
+ # Apply fixes
+ print(f"\n{'='*60}")
+ print("🔧 Applying Fixes...")
+ print(f"{'='*60}\n")
+
+ result = remediator.apply_fixes(fixes_to_apply, args.output)
+
+ if result['success']:
+ print("✅ Remediation Complete!")
+ print(f"\n📄 Output: {result['output_path']}")
+ print(f"\n🔧 Fixes Applied:")
+ for fix in result['fixes_applied']:
+ print(f" ✓ {fix}")
+
+ # Run veraPDF validation on result
+ print(f"\n{'='*60}")
+ print("🔍 Validating Remediated PDF with veraPDF...")
+ print(f"{'='*60}\n")
+
+ validator = VeraPDFValidator()
+ validation = validator.validate(result['output_path'])
+
+ if 'error' not in validation:
+ print(f"PDF/UA Compliance: {'✅ PASS' if validation['compliant'] else '❌ FAIL'}")
+ print(f"Passed Rules: {validation['passed_rules']}")
+ print(f"Failed Rules: {validation['failed_rules']}")
+
+ if validation['errors']:
+ print(f"\nRemaining Issues ({len(validation['errors'])}):")
+ for i, error in enumerate(validation['errors'][:10], 1):
+ print(f" {i}. Clause {error['clause']}: {error['description'][:80]}...")
+
+ if len(validation['errors']) > 10:
+ print(f" ... and {len(validation['errors']) - 10} more")
+ else:
+ print("❌ Remediation failed")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/test_visual_inspector_remediated.pdf b/test_visual_inspector_remediated.pdf
new file mode 100644
index 0000000..d90c9c9
--- /dev/null
+++ b/test_visual_inspector_remediated.pdf
@@ -0,0 +1,267 @@
+%PDF-1.3
+%
+1 0 obj
+<<
+/Producer (pypdf)
+>>
+endobj
+2 0 obj
+<<
+/Type /Pages
+/Count 3
+/Kids [ 4 0 R 14 0 R 19 0 R ]
+>>
+endobj
+3 0 obj
+<<
+/Type /Catalog
+/Pages 2 0 R
+/Lang (en\055US)
+>>
+endobj
+4 0 obj
+<<
+/Contents 5 0 R
+/MediaBox [ 0 0 612 792 ]
+/Resources <<
+/Font 6 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+/XObject <<
+/FormXob.2c2d8c1a59ccd390014a13df1823520c 11 0 R
+/FormXob.4239313bbffe37482d3f1e78247febb9 12 0 R
+/FormXob.c61c5faae8c5519bf83811c2a31afbe3 13 0 R
+>>
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+5 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Length 341
+>>
+stream
+GarWr9i&Y\$jPX:ItbE6&maiL1uX6udNf;FjhN`n',IsXJs
HT`hpOU*nK9/qZ*Zp?=GnqpB^3Zg\lWZTo68Cf!.WaZc`5in9GDZ%R(!@*)"BsDt
+endstream
+endobj
+6 0 obj
+<<
+/F1 7 0 R
+/F2 8 0 R
+/F3 9 0 R
+/F4 10 0 R
+>>
+endobj
+7 0 obj
+<<
+/BaseFont /Helvetica
+/Encoding /WinAnsiEncoding
+/Name /F1
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+8 0 obj
+<<
+/BaseFont /Helvetica-Bold
+/Encoding /WinAnsiEncoding
+/Name /F2
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+9 0 obj
+<<
+/BaseFont /ZapfDingbats
+/Name /F3
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+10 0 obj
+<<
+/BaseFont /Symbol
+/Name /F4
+/Subtype /Type1
+/Type /Font
+>>
+endobj
+11 0 obj
+<<
+/BitsPerComponent 8
+/ColorSpace /DeviceRGB
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Height 90
+/Subtype /Image
+/Type /XObject
+/Width 280
+/Length 2549
+>>
+stream
+Gb"0U$#g>t*!btg,d%GnKncJs5U@_PXUpaH)Ti3CWhW1eN^;K$ALJRAheM.!lABp.UPPpALo-1h8DKGcOG&E.+qjGBSbsfr41jtKHS9[,2rHrJ#asCm5A2"&B_B^UJ.5Pg)(W4tUjAf'D)"GAH+82g'Isrrd%Tku'ZgpDf*>*^&'j%Alo!_-k#Hm)R^:BuZ,#j5QMuHrS=0cl.$r(S`p^gCfHs!XaaZN9thnJDf_ha+TerJNh*iU_n0Nr1o`'5C=/bZ0)s,@upTEO@Flpm!P1EX/;nPE.^HpU/o>TODT3(;.]Cu2M]Akd,/Jj7EPmL@Y>H0!&eZ;jq+fa8Jn[CBSc,Q1K).J#A=+m2,O;58\$0Bi`mN;puBJ":)t<-J#>J6bcQhH*h^0%lD(/=]OH'\&."82dmjZ.`C>7g6kJ)pX?"an$5N;#3QFZB?@PQPGYrS.`bI^aWkASU`QnaS=_3k4rq,H=Y^H*,7oG8e96PJmMg]%oL[t94a2mP93T"<=b*@2CHaK)/-0:/YckY)m*Xs:n(?88?f*-*]dE_ec'g:C2nME;OZiZ53qY[;QRs0Anp`U3,gOOW-/dn,mD=RPe8p"]pDftG9"K3%J^k&?An!bFUU'am7l`)\PUY%:&W9?e;eG^SPk'ORW`@!%u6m4UX>FWL`\./VOOH?EZ6pGbl]+#V>8\%%a!W+Y859!RoWM=`LZ_-IFQ<;tIiH*8;165`ZcH7A1_%^V<[dFu,8P&XP,q?=noK,(DQ6tW+BP`'Gl.0^`]"RWT#)jC1X0AhA;IVB[4ZoCIdI:%'pUJ'VX&1>O].]/`'7l!M*8b!Z\Ge$!ZlINXb/pOWe()f(nX)9V0hH8f#d_,B`o=6g"F_H;XO]@>0%imb"5p<*Z(h=CCO,WrR3,k]SrrISN>0-sjTF?%48&^T(o158niPLMfCY/:31m$<.AA3-bIMMP:aNZ:q275KfLCO,`hm:OrEcTsc0B(R-UMJK<;NEE3`BQa[L8)>1s0Y;;,D1HX^!l'<$)W^5NY\8,R59hi8&^]+o10b'M-dk>1_!Kg*2qBTgt>,%eZ%#8'L$m+ThK+KW`Hg"S*Qph$JN_!ZY(5GTjDdJYj1?`AuU64U9-^Mn7;[l;Dh_?jHMCBq8Of;`G,\%Yo^SY&OrrUXqrJ$d%;VStd;`$I^3`%91R7HfWl.ii0ACVh%6!fijL!CoqI`du$P.])`/%K-.T]"`FClZ-3O&&B/*a@`&:Rq3AGuRHPrI&TAjgRd#ED?)5Ln*YS91]4RUJd+\O5+V,`N[q"nk0>OeJap&,i=&W\F?Z60lA!2Pq"r4:p]A2A??rhTN&'b(9LpAQ&!C9gsDHZ`K>65-m0X=)Io"@YsE2B&8L[iX/_a2N?((kL$@jPXSj]qPlEREI^q7Meot#$1QUVk9n;Jna]A>Wd%SX?Sk%B.;1sZn7RZl@9(L6P/tJEpKf$hh[s@T*;MuPMO,/UJLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCM!1,r+3k=+Zi~>
+endstream
+endobj
+12 0 obj
+<<
+/BitsPerComponent 8
+/ColorSpace /DeviceRGB
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Height 120
+/Subtype /Image
+/Type /XObject
+/Width 350
+/Length 2263
+>>
+stream
+Gb"0UH#+0p*5M)GH>j0WTFrdu!g24eE`>HpUC[t>'p3IV%>':aW)s+$0lf["&PF]GM:%uQ_8O8"9oPfDs6tg_K/`R\)@sIqlL4BTh4<6Ph)?@cgR@Tlo>g:bRsjmWn$g'"g"f[MV^>/De,dTs"dW/n=tYn@@IYt^3f"@Ih/A?Y]VGp81uG[peeoHYgio'hm`&MIoP`;r/kknsZL?SBHJbh\e9?tPU-dD(Q"lPcpYA$^kFD>#2DouOmZWj2:RsH:=3!s=*D5MZ=-M86YuE:mV>CthWtA3qhm*"QghM7'CW;XWP?[gWX45f0n*F)8;h#fa%np!ZoCPH3Q"LM'-[/"j,p(#\L5AEgdbd,So\Dp[JeN2#Cgn571;7rG8S;JH,"St`=Y5Ok\=5D^p%B+lkYTMR>AL_IXTH)G$ZXci_^=fL)L:EjRV!Bd(V9fbeeftOCIac\j;'chH1e#Ue[9@cd2K4Fr!a)n!p&bgn@MDEqV5'I;66tYGhqu%9.4dp!e$T9:>X"[ltDF?F"F:k&gK8LOO6r-MLF\CfGoP=!tGV'kThlUJEQ1tSlM_cum"<&&$L_map'IJT$]MO\$'cR$?=G@b$[Gl5d5M?TNRN7Z)Ht[4f51-X?2?jF-N;'7m:-%G"'$G=S)fXD\;g6SI%Z'E-]4)q2K%gSWVb$[#_V_Wo9:71.LN+(/W?pBQ7YsKqZbNc&1Y&8e?_p2CK".>4mb870k=6Ts1\a+T)-8">6[k_?&G^QL>.-J)dU\*a=a%Q&;B]^fF:M'%>Y-N4#K?Yg9aq-`r@@#4pL.NnJr@A#h$E6uDQ!sV*T7K&4d=43g9"hrF5A6/;o1ceAU%q+Q[<;=[TZYWn]l'7b8,_Is=io3?<#NOX-d;-a`\;+o&MFro02?daHuAcFurlMY0"e+^;[Oa$th&[f6h:l[r_;VqG\?L#H,SbB-5$eQ,.nbJRX=4Wf>/_Q0J,`:+RHcg[dKd:X-(S`a.OdR.48CG.DcR:[K[Mfa?n(G=fI2Sk"[.T(Sp8KF^h;Qd7jM2W%\Ac6?)dO@loX).`'#X++Y1kCljHohQdV6O0JW2?-+5R^$r32OZ](SrA7C$/D)7*C.tX"bNQSJCZ;,PaW7K48VY08N^RL6(qH1#:[Zn7US:L06WbDRKs)OL"1.Y3O2_eCKeaM2O-2O^p3(MRHGp$`VC&G)8MOe2W2sU\IlE0Yn(%I$QMZNK!=U<$e)(ckSi0
+endstream
+endobj
+13 0 obj
+<<
+/BitsPerComponent 8
+/ColorSpace /DeviceRGB
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Height 100
+/Subtype /Image
+/Type /XObject
+/Width 300
+/Length 1451
+>>
+stream
+Gb"0U:P__`(r5Yt,l\28,"<@I,_]>K;\UNM/2/TUKS@F<@6n$%)pH;'AY[?(A8K7P(5Ke+`K+F5HMYqn-F1kFQb_3ELetzzzzzzzzzzzzz!!!!-Pbh%7gc9ZY>2%[UT9kiZ\T1,>XXXaK2c%;%cCBBH/t-eFec]6>'O%iZi'(MUc*'J&[3496qX)6O+hJ>\EHSBoUnR_+Kbqs:Y#oqn6ih=9Bg/T8Il`+05Eg?K6mr9bhg$:!;X9d+($j:okI^Hj2U>`:CfL^$[VL(Ue1.BQ#4Sp(A"+/NA#QqU9RQF/%nh'A&=\6X\H'Y:CfL^Me84R5>\JbQA6"DkHA7c_jS6O6N>j`9\Y3W^<+BS?7Csjc^sB.3T3oZhL'Xr+^Hq"Bu!H4FC`rRq=RBNU)u'9)"?iWF7QkXR6?\XkOm?S3<_2#k"RFXqYI"T>g=+(u9T'rLcIIk`HASr$aF7QC(.0oUoX<7Er,d]6alq9P(&K4RBk7pje5/H2:JBbTSMWn``>pF@#G0eRm.Yo/a3?IOp*V-V^@`H8'`VDU0Bu'ZclPB=.rfjd!Aal+Qc2&`)0kV2m]m,G*5]V+haO5-nO!CH!7tS2?5rl+ukHps:2Y'Z_>:b1G.=ARLNpF`k!'OcGA?.8,uJ[;33mUPCGI*_`%U8F/W`7bZLnRlWWBn`$:l.%_Oh5LDW6_EA&X.@7BuAa$gLF;.bToM.^=daI\F3;0sWWR:sH^-?;f$GnUIQS8#9;6dlD^OCCKS-aD[QgDPC"q>6`Q8)kh;]r-bL"NtZEoVmTO_KL;hrXZT/]\ec$7#Lr0NG]W<"BoEpY15IVrIm%V[(P
+endstream
+endobj
+14 0 obj
+<<
+/Contents 15 0 R
+/MediaBox [ 0 0 612 792 ]
+/Resources <<
+/Font 6 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+/XObject <<
+/FormXob.1310210de56a359f75cadd6058093d5c 16 0 R
+/FormXob.85598c76e5387c61e079109a4090d1fe 17 0 R
+/FormXob.fe6121c1aa08a49ce6c0bd2422036546 18 0 R
+>>
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+15 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Length 344
+>>
+stream
+GarWs9hPRC&-h(ireg6C@b[=(,b'$WZqsRqaMDY\bhC3WKAA-SoA/g1NJ)uDKfj9?JA\,A)-_W,%uV_71&)YXbn^"8\FmfqB4*UZD!1LRV[l*=<,/qp_WaF4(>qiqc[,[GDuFLaS#tC!?$4sh\hih/i6T1!ru6I11s&fn"1a/8,Fq*/abM4Z=s1c_&/sbfWXIJ@*k#Q]GOhNl[:$otBErSq[H$5h`F>80m8I?;W?c#k,hdoL]=QEFUh!;+FCil4DK>8,14!Eb`$k;JWPoEIU_(lWjeA,ulbnYu9;@dJA4iG\d24hBH&gG/fiT->V6-I8_9*A$T[7,A=saK3GDm#MXT~>
+endstream
+endobj
+16 0 obj
+<<
+/BitsPerComponent 8
+/ColorSpace /DeviceRGB
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Height 80
+/Subtype /Image
+/Type /XObject
+/Width 200
+/Length 1760
+>>
+stream
+Gb"0SHUnlS*!btK%spT278X2APSBr^+VdBXo_M3)&dk?LrDb",77$mGWO]17lYB4#;)>3%bSOEbO!W"Th-+sQopKFU[<0sbgT0/2GJACT__fZh74r[f^;G_nF3\\DS,%*ebc(-al%k.OLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLknUFdH%':2/+Xj/L0D?U!H(`SMcPE7;i!2gZ1uM`-+3?['^uUfj9Mei0%Kqg_[`OU:&rJNJ>IBZLB_;CQsT)lOP9^Z?DP)0frt"_5)_7b2US(1s\@2S)Soc1GHj^:4,LCk+stsS%W0TX6OPe/%N%u[QB1'ahsD:d;Pe^S].eR:GZ(oIjUp<[kUr@RB*OQc7aB\1Qa2([\Q]!WE`n%$X:JH`.Hf-pkQ$@Cla,]7W#ls#_nR4E*JhDk=_^$67ImA%Q*jsPZo%EU?hs^V7pj9XrAn9mOn#o+Z#1X./oD1%_XGSa;L)/*tl3eRO)Igg9(c=9P?3YHHNu1Rbk[:LU).nsp'X5g\g>O2il4>c$jN#+ba!Un$eOd_gRU^&Q7o_YY.B^%6afL%=4PVV=.1'pFZ/9]no/0CG/`gb:304;ZCn#$"J'dIeM1-KDm%FAh*:?$HJoT?*`o?p*B"@bRu?Hl?]gtdniu7Do:BVjqu$jpoW,N(jl+?e!CDKg"ACZ(ICB\`Pi!RMX4[[&.D,c&rZ3S-Z#\YQemm1kb.l#)1p*m`Q3Jm/OqT>Z`T[-Ao;[,a`4UkR4:jq[I$]Y7)^CfqeLZtcQ_h8fh8A(4_>Ucb8<]_R"h+hVM<<=RG29o?af>BD\P6mk=aqRaJ4RZAnBI\?g0C2j3+JBOMi:anWH&.SAJ&V82n>#m!BWl&,fq4lb!+ci9\`S:HDRo.BQZsTMri-ss5GA_qi3e;l504J.+=N^E]A3E0HK76j^T!CH)c0nj.>1hAlV?$:.#M7PTM3=/,P"?esj*,QAN@/4RI=sXK:J,?`0/>^^Hh!HrBo2g!.~>
+endstream
+endobj
+17 0 obj
+<<
+/BitsPerComponent 8
+/ColorSpace /DeviceRGB
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Height 100
+/Subtype /Image
+/Type /XObject
+/Width 320
+/Length 2098
+>>
+stream
+Gb"0UBiEMR(l%"a5&LXl$S!>%iiZ9`.U&YaY./u`/g0/6Q90sJ14UL"F.VBnPD\TMe!!(WkM4Z:dW?k)VOqsC&dedBzzzzzzzzzzzzzz!!!AhcCHJPbE!]-qU6h_PKfRUQ^@*q]/Q_7]6E^CTs(Zg2kCib%eoDIe(Ap=m+JSpq:`5lD/a6SregYUF^4Kqs.96dIe/EoU))hacQ^-&KpuFRB54fT=gR8-KaSP-';\T@AnGY"G^.]7:#WO'F`l<=?2O9YP:A+7/81DnGB^*QrV$'YkN/==RSKD7V4[53\PljB5;4da4[4Ak<968+4Y"3rs*j#;X^(P:L"j(TfD-,=`MKE-l07H+TqQ>X[\Y')G5^4KfQd;emA['6qi%;FUMXjbiIbd15JIF6*n6:0m/6bTml(+Ao=Jqu5.,DqJjNOUDtnEFXN^LjQ06>W09KcCq!g^!*7RRFCC'@t2Fm`mj]D'j&(_d=),[*mgSP1T2Dmn?Fj?L;'<[O^hoO_/Gir/O?#==ILF4s5>"_n]/($r/NTBibX]sM,oB&bXEpW8`=8Bmt+04Z9cOOpuq5l'8hp\K!X(6*cDSq2=j()&k#J/QGIOk`QgCc'fWOpdNr2Pmn*&tKUo',R?+OlO)&X>2$;V3h1GbJJ>$>*]-7Sb5T31pM=$t7[Lm2h2P)^L,n,E:_p%,Y2hdW)09PRM=B5`$M\gNVA=%BG='&IirH:X#X)T*>pX#U$qK[(#1&XI>bcgE3==hHMf4nI'4aQa6[CoGB6X1N"X+bu@!8?EU[]B@r,QDN&Da4]XcH]0(Bq!XSY?kL:U&5B2%gVpd]mI6UYeIh8j[3%lDgQiC--ORi9Zp*+DHY$&g`&g*il[?ih4.Z4MG*ToY4cdor*-/uRYHP$)uLJAWq*3)WuUk_o&n>kKD]KNg&;L%3"W'd>L?j,XI.mQ5Ak3G+$Qds;Q43QG&-=O[mOERSf^[$9pJX!9:;TYp2#cebEcM;'(tk_ltg39Z-fK^CYoM"Q!Z&ncjJ[bl:0"k&3N/q)]Nj.hU^7ia0g,cI%DG5pXN'24GfTJj2[5(b7B=*Hc*Tc>hS[`pCo*=[.(Q4p/j6N<&A;c=TmgJc't011du.7e]Q@ted1j$dEuCQH@("(QS0\<6^Wfs<'91?d*Yrl3mSTZ+%D\[e`snF)HpD#)TdT:;<=OC/NZda^P+Oajdos0fAEWg`(adP<,I^V;uQ,2'A>fD,-N%IAuJeO7d5e"ckTDd&(U]mEh-;jtkSs"A%krSkeSq>#Z/g`^Y*/:0YceRXW]%X&Bzzzzzzzzzzzzz!!!#WYOE("02E8~>
+endstream
+endobj
+18 0 obj
+<<
+/BitsPerComponent 8
+/ColorSpace /DeviceRGB
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Height 90
+/Subtype /Image
+/Type /XObject
+/Width 250
+/Length 2270
+>>
+stream
+Gb"0TI8!XP*!bu?=)2B:rFIL[/XdQTi]!gmb[^Idi+ta!1:qXS4:d@8L'MpPrJg`9]G_&*oj[B;t]5lk:?7t$ILI[@8kAi3\CIb/gh.Q)Ekgc>*2Vn1,m/Gn3kt8li['\Hn5te(OPl/pWGa:.L7N>_;@'9@<[JIfm!Y;Fq#iQ>*W-"?9%?^H5lQWk=lTn6C@jE`DEN@Y6eMrn/d0i\NHOV7gu!C#d$!c-s:"Fp6_:k[T8imJ(imbu`b$:NMTpr=[DAT>d[e:Mt8r3&G@,o^\lq^-V.Z8)/H?fJDcrV_gUnVVOd(duGZT-kBK3>2u38o=s-HoZkh#'HO)g7iPAqb*?VHJ-=VTGHa%]JF'3,%lla\.dQTcMN;e)ejTWs:%[[umnS*_+Za2jAnhE\CDT?cfD27\&:WLNs_X7auj8$^d=E\jJjg;5%@nm"!I^E'mX,_Qe&oVaV4_kS13@#q!q9s7W0Q=V70^V?XRgka/d9qO+'F+WK,Cnma+q_KLX-/jm#i@42rBQm+X_ZVL=*kL5UK>;"%oHQTRK+]92`]*Tq!u(?gCneoRmJNV7C/L2"P8)itN!c#Kl;?%8Q@eYKmPTL#nCO`pQK:Y>[:G-j1KC@^n$jKsQ]3UQ)WXLrhTkAL1Nqp](e_I6for/>(/,QZ*6DWc\b(&-m8I'UZEYbsNH18`kuHI@h;pnXOZH6&@OI_'4/n[p-QAEOajbmVe+LoX:Set;ZYPY+[I-);QJW*%($W`ZD'UE6ImY9f'+3UL&-fRd[]Mg`IuMJk,M8%]:X9(SgoZl;S4g4NuBM*C5I>sIQ`gQ!_l->Kl%='W'uDQh0\0f\R!VF47Uk!oU$#tFHDU\BX]08rLu]D,]k%$.>kOVK7@+pU91[Q?,6QDZ>O,qk&.sg4Q*]br2pUa\[#&)fll[H8)WI:\/C:U4Z]YGM+6U9^"OU"r0`)g?f3J@+Ci'L9m(mB-5CW(].TGe^7*=S;MTPi2Rh6P+rr"A(6QcGDq]71jX+KFt[W)E.je3]n![peTp*t>+'88?kl4`HDs4l]n*a"b`C6WIld>bWJ(Y'u_7%uuW0hrKT)nOnirBfD%MCo!"GD;9O\:"i=i%pST,'b75d[?%e*l^o7.rXYfeoV^M%qTF529R4sP*n7Ig(40>)S[_Ul@:!We&UqeUjQpnr+naYj1^;eRLcPQ4'N$S9m>8"nMT59!dcGYu[$sMuMpfSliP7EmKkjDgWjh9t+)0=k5;K+,LkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkpkCLkr2^IfkUlr,2~>
+endstream
+endobj
+19 0 obj
+<<
+/Contents 20 0 R
+/MediaBox [ 0 0 612 792 ]
+/Resources <<
+/Font 6 0 R
+/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
+>>
+/Rotate 0
+/Trans <<
+>>
+/Type /Page
+/Parent 2 0 R
+>>
+endobj
+20 0 obj
+<<
+/Filter [ /ASCII85Decode /FlateDecode ]
+/Length 442
+>>
+stream
+GasbV92EDi'SZ;\MW51?/=k35\e>/!#\\19)`FO!BXP%f9\#d(oV'c<'%:B[h"6!gSBbOsou"r$O+@VX@*ZP=n/[m5f\d.]pdmKT@+iNS)B7_SSCInc`.b=90mXAeShRgo1_kUi"ZO^NMCDDo$Ibd]rX+,JKC*!s`3K`nK2oG>q4iWhFc1hYI4r'_j8bX;T\rNki)>`]lI15^[ObkfsST8VodBK%7U*+4ust^O'%Jk&hHsIW1DRX-QC5H*H?@\rGCjBpH>n
+endstream
+endobj
+xref
+0 21
+0000000000 65535 f
+0000000015 00000 n
+0000000054 00000 n
+0000000127 00000 n
+0000000193 00000 n
+0000000544 00000 n
+0000000976 00000 n
+0000001038 00000 n
+0000001145 00000 n
+0000001257 00000 n
+0000001340 00000 n
+0000001418 00000 n
+0000004156 00000 n
+0000006609 00000 n
+0000008250 00000 n
+0000008603 00000 n
+0000009039 00000 n
+0000010988 00000 n
+0000013276 00000 n
+0000015735 00000 n
+0000015926 00000 n
+trailer
+<<
+/Size 21
+/Root 3 0 R
+/Info 1 0 R
+>>
+startxref
+16460
+%%EOF