Phase 1: De-Oliver rebrand — remove Azure AD, GCP, Oliver branding

- Delete PHP API layer (api.php, auth.php) — replaced by FastAPI in Phase 2
- Delete MSAL/Azure AD JS files (app.js, app-history.js, api.js)
- Delete GCP Cloud Build/Deploy infra (cloudbuild.yaml, deploy.sh, Dockerfiles)
- Delete Oliver-specific docs (OLIVER_CUSTOMIZATION.md, DAVE_QUICK_SETUP.md, etc.)
- Replace Oliver yellow #FFC407 with Aimpress indigo #6366F1 across CSS + reports
- Replace Oliver Solutions footer in report_generator.py with Aimpress
- Switch font from Montserrat to Inter in CSS
- Replace GCS optical-pdf-images bucket with STORAGE_BUCKET env var
- Rewrite README.md for Aimpress SaaS product

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-19 14:41:27 +01:00
parent cfa7eeeeac
commit 5a00ec88d7
33 changed files with 59 additions and 6831 deletions

View file

@ -1,29 +0,0 @@
FROM python:3.11-slim
# Install system dependencies for PDF processing
RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-eng \
poppler-utils \
ghostscript \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies
COPY requirements-cloudrun.txt .
RUN pip install --no-cache-dir -r requirements-cloudrun.txt
# Copy application code (no worker, redis_queue, or db_manager)
COPY cloudrun_service.py .
COPY enterprise_pdf_checker.py .
COPY pdf_remediation.py .
COPY logger_config.py .
COPY retry_helper.py .
# Cloud Run sets $PORT; gunicorn binds to it
# --workers 1 --threads 1: Cloud Run concurrency=1, one request at a time
# --timeout 900: allow up to 15 minutes for large PDFs
CMD exec gunicorn --bind :$PORT --workers 1 --threads 1 --timeout 900 cloudrun_service:app

View file

@ -1,27 +0,0 @@
FROM php:8.2-fpm-alpine
# Install Nginx, Python (for report generation), PostgreSQL libs, and PHP extensions
RUN apk add --no-cache nginx python3 postgresql-dev && \
docker-php-ext-install pdo pdo_pgsql
# Copy Nginx config
COPY nginx.conf /etc/nginx/http.d/default.conf
# Copy application files
WORKDIR /app
COPY api.php auth.php index.html ./
COPY report_generator.py ./
COPY css/ css/
COPY js/ js/
# Create directories
RUN mkdir -p /app/uploads /app/results /app/logs && \
chown -R www-data:www-data /app/uploads /app/results /app/logs
# Start both Nginx and PHP-FPM
COPY docker-entrypoint-web.sh /docker-entrypoint-web.sh
RUN chmod +x /docker-entrypoint-web.sh
EXPOSE 80
CMD ["/docker-entrypoint-web.sh"]

View file

@ -1,31 +0,0 @@
FROM python:3.11-slim
# Install system dependencies for PDF processing
RUN apt-get update && apt-get install -y --no-install-recommends \
tesseract-ocr \
tesseract-ocr-eng \
poppler-utils \
ghostscript \
libgl1 \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY enterprise_pdf_checker.py .
COPY pdf_remediation.py .
COPY logger_config.py .
COPY retry_helper.py .
COPY redis_queue.py .
COPY db_manager.py .
COPY worker.py .
# Create directories
RUN mkdir -p /app/uploads /app/results /app/logs
CMD ["python", "worker.py"]

View file

@ -1,284 +0,0 @@
# 🚀 Quick Setup for Your MAMP Configuration
## Your Setup
- **MAMP**: Points directly to project folder (no copying needed)
- **venv location**: `/Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker/venv`
- **Google API**: Using API key string (not JSON file)
- **Anthropic API**: Using API key string
---
## ✅ What's Already Configured
The code is now hardcoded with your venv path:
```php
// In api.php - already set to your path
$venv_python = '/Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker/venv/bin/python3';
```
**This means:**
- ✅ No need to edit `api.php`
- ✅ No need to configure venv path
- ✅ Just point MAMP to the folder and go!
---
## 🎯 Installation (5 Minutes)
### Step 1: Create venv
```bash
cd /Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker
# Create virtual environment
python3 -m venv venv
# Activate it
source venv/bin/activate
# Install dependencies
pip install -r requirements.txt
# Deactivate (optional)
deactivate
```
### Step 2: Get Your API Keys
#### Anthropic Claude API Key
1. Go to: https://console.anthropic.com/
2. Create an API key
3. Copy it (looks like: `sk-ant-api03-...`)
#### Google Cloud API Key
1. Go to: https://console.cloud.google.com/
2. Enable "Cloud Vision API"
3. Go to "Credentials"
4. Click "Create Credentials" → "API Key"
5. Copy it (looks like: `AIzaSy...`)
### Step 3: Point MAMP to Your Folder
1. Open MAMP
2. Preferences → Web Server
3. Set Document Root to:
```
/Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker
```
4. Click OK
5. Start Servers
### Step 4: Access the App
```
http://localhost:8888/
```
---
## 🎨 Using the App
### Option 1: Web Interface (Easiest)
1. Open: `http://localhost:8888/`
2. Drag and drop a PDF
3. Enter your API keys in the form:
- Anthropic API Key: `sk-ant-api03-...`
- Google API Key: `AIzaSy...`
4. Wait for results (2-5 minutes)
5. Review accessibility report
**Note:** You can also set API keys as environment variables (see below) and leave the form fields empty.
### Option 2: Command Line
```bash
# Activate venv
source venv/bin/activate
# Run checker (replace YOUR-KEY with actual keys)
python enterprise_pdf_checker.py your-file.pdf \
--anthropic-key "sk-ant-api03-YOUR-KEY" \
--google-key "AIzaSy-YOUR-KEY" \
--output report.json
# Deactivate
deactivate
```
---
## 🔐 Setting API Keys as Environment Variables (Optional)
If you don't want to enter keys every time:
```bash
# Add to ~/.zshrc (or ~/.bashrc if using bash)
echo 'export ANTHROPIC_API_KEY="sk-ant-api03-YOUR-KEY"' >> ~/.zshrc
echo 'export GOOGLE_API_KEY="AIzaSy-YOUR-KEY"' >> ~/.zshrc
# Reload
source ~/.zshrc
# Test
echo $ANTHROPIC_API_KEY
```
Then you can leave the form fields empty - it will use the environment variables.
---
## 📁 Your File Structure
```
/Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker/
├── venv/ ← Python virtual environment
│ └── bin/python3 ← This is what api.php uses
├── uploads/ ← Created automatically
├── results/ ← Created automatically
├── .cache/ ← Created automatically
├── index.html ← Web interface (Oliver branded)
├── api.php ← Backend (hardcoded to your venv)
├── enterprise_pdf_checker.py ← Main checker (Claude 4.5)
├── requirements.txt ← Dependencies
└── [documentation files...]
```
---
## 🎨 Oliver Branding Confirmed
**Colors**: Black (#000000) + Yellow (#FFC407)
**Font**: Montserrat
**AI Model**: Claude Sonnet 4.5
**Your venv path**: Hardcoded in api.php
---
## 🐛 Troubleshooting
### "Python script error" or "command not found"
```bash
# Check venv exists
ls -la /Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker/venv/bin/python3
# If not, create it
cd /Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### "Google API error"
Make sure you've:
1. Enabled Cloud Vision API in Google Cloud Console
2. Created an API key (not service account JSON)
3. The API key has Vision API enabled
### "Anthropic API error"
Make sure your API key:
1. Is valid (starts with `sk-ant-api03-`)
2. Has credits/billing enabled
3. Is typed correctly (no spaces)
### "Upload failed"
Check MAMP is running:
1. Open MAMP
2. Make sure Apache is green
3. Make sure port is 8888 (or adjust URL)
### Permissions errors
```bash
cd /Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker
mkdir -p uploads results .cache
chmod 755 uploads results .cache
```
---
## 💡 Daily Workflow
### Starting Work
1. Open MAMP → Start Servers
2. Open browser → `http://localhost:8888/`
3. Upload PDFs and check!
### For Python Development
```bash
cd /Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker
source venv/bin/activate
# ... do your work ...
deactivate
```
### Ending Work
1. MAMP → Stop Servers
2. Done!
---
## 🎯 Test It Now
1. **Open MAMP** → Start Servers
2. **Visit**: `http://localhost:8888/`
3. **Upload** a test PDF (use sample_good.pdf if needed)
4. **Enter API keys** in the form
5. **Click upload** and wait
6. **Review results**
Should take 2-5 minutes for first check (with caching, repeat checks are faster).
---
## 📊 What Gets Checked
- ✅ Document structure & tagging
- ✅ Text extractability
- ✅ Image alt text (with AI)
- ✅ Color contrast
- ✅ Readability scores
- ✅ Form field labels
- ✅ Link quality
- ✅ Heading structure
- ✅ OCR quality (if scanned)
- ✅ 30+ other checks
**Coverage: 95% of WCAG 2.1 Level A & AA**
---
## 💰 Cost Per Check
Average 10-page PDF with 5 images:
- **Anthropic Claude**: $0.075 (5 images × $0.015)
- **Google Vision**: $0.008 (5 images × $0.0016)
- **Total**: ~$0.08-0.10 per document
First 1,000 images/month on Google are free!
---
## 🎉 You're Ready!
Everything is configured specifically for your setup:
- ✅ venv path hardcoded
- ✅ MAMP-compatible (no ini changes needed)
- ✅ Google API key support (not JSON)
- ✅ Oliver branding applied
- ✅ Claude Sonnet 4.5 enabled
**Just point MAMP to your folder and start checking PDFs!** 🚀
---
## 📞 Quick Reference
**MAMP URL**: `http://localhost:8888/`
**venv Path**: `/Users/daveporter/Desktop/CODING-2024/PDF-Accessibility-checker/venv`
**Activate venv**: `source venv/bin/activate`
**Deactivate venv**: `deactivate`
**Get Anthropic Key**: https://console.anthropic.com/
**Get Google Key**: https://console.cloud.google.com/ → Credentials
**Need help?** Check the other docs or the troubleshooting section above.

View file

@ -1,502 +0,0 @@
# 🚀 MAMP Setup Guide - Local Development with venv
## Overview
This guide is for running the Enterprise PDF Accessibility Checker locally with:
- ✅ **MAMP** - Apache/PHP stack
- ✅ **Python venv** - Isolated Python environment
- ✅ **Oliver Branding** - Black (#000000) and Yellow (#FFC407)
- ✅ **Claude Sonnet 4.5** - Latest model
---
## 🔧 Quick Setup (10 Minutes)
### Step 1: Install System Dependencies
```bash
# macOS
brew install python3 tesseract poppler
# Ubuntu/Linux
sudo apt-get update
sudo apt-get install -y python3 python3-pip python3-venv tesseract-ocr poppler-utils
```
### Step 2: Create Python Virtual Environment
```bash
# Navigate to your project directory
cd /path/to/enterprise-pdf-checker
# Create virtual environment
python3 -m venv venv
# Activate it
source venv/bin/activate
# Your prompt should now show (venv)
```
### Step 3: Install Python Dependencies in venv
```bash
# Make sure venv is activated (you should see (venv) in your prompt)
pip install --upgrade pip
# Install all dependencies
pip install -r requirements.txt
# Verify installation
python enterprise_pdf_checker.py --help
```
### Step 4: Configure API Keys
```bash
# Set API keys in your current session
export ANTHROPIC_API_KEY="sk-ant-api03-YOUR-KEY-HERE"
export GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/to/google-credentials.json"
# To make permanent, add to your shell profile:
echo 'export ANTHROPIC_API_KEY="sk-ant-api03-YOUR-KEY-HERE"' >> ~/.zshrc
echo 'export GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/to/credentials.json"' >> ~/.zshrc
# Reload your shell
source ~/.zshrc
```
### Step 5: Set Up in MAMP
```bash
# Option 1: Copy to MAMP htdocs
cp -r /path/to/enterprise-pdf-checker /Applications/MAMP/htdocs/pdf-checker
# Option 2: Create symlink
ln -s /path/to/enterprise-pdf-checker /Applications/MAMP/htdocs/pdf-checker
# Create required directories
cd /Applications/MAMP/htdocs/pdf-checker
mkdir -p uploads results .cache
chmod 755 uploads results .cache
```
### Step 6: Configure MAMP
1. **Open MAMP**
2. **Preferences → Ports**
- Apache: 8888 (or your preferred port)
- PHP: Default
3. **Preferences → PHP**
- Version: 7.4 or higher
4. **Start Servers**
### Step 7: Update api.php for venv
The PHP script needs to know about your venv. Update the Python command:
```php
// In api.php, find the command building section and update:
// Path to your venv Python
define('PYTHON_BIN', '/absolute/path/to/enterprise-pdf-checker/venv/bin/python3');
// Build command using venv Python
$cmd = escapeshellcmd(PYTHON_BIN . ' ' . PYTHON_SCRIPT) . ' ' .
escapeshellarg($pdf_path) . ' ' .
'--output ' . escapeshellarg($output_path);
```
Or use this complete replacement for the check command section in api.php:
```php
// Build command - use venv if available
$venv_python = __DIR__ . '/venv/bin/python3';
$python_bin = file_exists($venv_python) ? $venv_python : 'python3';
$cmd = escapeshellcmd($python_bin . ' ' . PYTHON_SCRIPT) . ' ' .
escapeshellarg($pdf_path) . ' ' .
'--output ' . escapeshellarg($output_path);
```
### Step 8: Test Installation
```bash
# Activate venv (if not already active)
source venv/bin/activate
# Test Python script directly
python enterprise_pdf_checker.py --help
# Test with a sample PDF
python enterprise_pdf_checker.py sample.pdf --output test-result.json
# Deactivate venv when done
deactivate
```
### Step 9: Access Web Interface
```
http://localhost:8888/pdf-checker/
```
---
## 🎨 Oliver Branding Applied
The interface now uses your brand colors:
- **Primary Color**: Yellow (#FFC407)
- **Secondary Color**: Black (#000000)
- **Font**: Montserrat (all weights)
### Design Elements:
- ✅ Black header with yellow accent
- ✅ Yellow primary buttons with black text
- ✅ Black/yellow score display
- ✅ Montserrat font throughout
- ✅ Professional, clean aesthetic
---
## 🤖 Claude Sonnet 4.5
The system now uses **Claude Sonnet 4.5** (`claude-sonnet-4-5-20250929`) - the latest and most capable model:
**Benefits:**
- Higher accuracy for image analysis
- Better alt text suggestions
- Improved context understanding
- More nuanced accessibility recommendations
**Cost:** Same as 3.5 Sonnet (~$0.015 per image)
---
## 🔄 Daily Workflow
### Starting Work
```bash
# 1. Navigate to project
cd /Applications/MAMP/htdocs/pdf-checker
# 2. Activate venv
source venv/bin/activate
# 3. Start MAMP
# (Use MAMP application)
# 4. Open browser
open http://localhost:8888/pdf-checker/
```
### During Work
```bash
# Python changes require venv to be active
source venv/bin/activate
# Test Python script
python enterprise_pdf_checker.py test.pdf
# PHP/HTML changes work immediately (just refresh browser)
```
### Ending Work
```bash
# Deactivate venv
deactivate
# Stop MAMP
# (Use MAMP application)
```
---
## 🐛 Troubleshooting
### "command not found: python"
```bash
# Make sure venv is activated
source venv/bin/activate
# Check Python path
which python
# Should show: /path/to/enterprise-pdf-checker/venv/bin/python
```
### "Module not found" errors
```bash
# Activate venv first
source venv/bin/activate
# Reinstall dependencies
pip install -r requirements.txt
```
### PHP can't find Python script
Check in `api.php`:
```php
// Make sure paths are absolute
define('PYTHON_SCRIPT', __DIR__ . '/enterprise_pdf_checker.py');
// Use venv Python
$venv_python = __DIR__ . '/venv/bin/python3';
$python_bin = file_exists($venv_python) ? $venv_python : 'python3';
```
### API keys not working
```bash
# In the web interface, you can enter keys directly
# Or set them for the PHP environment:
# Add to .htaccess (in project root):
SetEnv ANTHROPIC_API_KEY "sk-ant-..."
SetEnv GOOGLE_APPLICATION_CREDENTIALS "/absolute/path/to/creds.json"
```
### Permission errors
```bash
# Fix directory permissions
cd /Applications/MAMP/htdocs/pdf-checker
chmod 755 uploads results .cache
# If using Apache:
sudo chown -R _www:_www uploads results .cache
```
### Font not loading
The font is loaded from Google Fonts CDN. If you need offline:
```html
<!-- Download Montserrat and add to project -->
<link href="fonts/montserrat.css" rel="stylesheet">
```
---
## 📝 api.php Configuration for venv
Here's the complete updated section for api.php:
```php
/**
* Handle PDF accessibility check
*/
function handleCheck() {
$job_id = $_POST['job_id'] ?? '';
if (empty($job_id)) {
error('Job ID required');
}
$meta_file = RESULTS_DIR . '/' . $job_id . '.meta.json';
if (!file_exists($meta_file)) {
error('Job not found');
}
$job_data = json_decode(file_get_contents($meta_file), true);
// Get API keys from request or environment
$google_creds = $_POST['google_credentials'] ?? getenv('GOOGLE_APPLICATION_CREDENTIALS');
$anthropic_key = $_POST['anthropic_key'] ?? getenv('ANTHROPIC_API_KEY');
// Build command - use venv Python if available
$pdf_path = $job_data['filepath'];
$output_path = RESULTS_DIR . '/' . $job_id . '.result.json';
// Check for venv Python
$venv_python = __DIR__ . '/venv/bin/python3';
$python_bin = file_exists($venv_python) ? $venv_python : 'python3';
$cmd = escapeshellcmd($python_bin . ' ' . PYTHON_SCRIPT) . ' ' .
escapeshellarg($pdf_path) . ' ' .
'--output ' . escapeshellarg($output_path);
if ($anthropic_key) {
$cmd .= ' --anthropic-key ' . escapeshellarg($anthropic_key);
}
if ($google_creds) {
$cmd .= ' --google-credentials ' . escapeshellarg($google_creds);
}
// Update status
$job_data['status'] = 'processing';
$job_data['started_at'] = date('Y-m-d H:i:s');
file_put_contents($meta_file, json_encode($job_data, JSON_PRETTY_PRINT));
// Run check in background
$cmd .= ' > /dev/null 2>&1 &';
exec($cmd);
success([
'job_id' => $job_id,
'status' => 'processing',
'message' => 'Check started'
]);
}
```
---
## 🔐 Environment Variables in MAMP
### Option 1: .htaccess (Recommended)
Create `.htaccess` in project root:
```apache
# API Keys (don't commit this file!)
SetEnv ANTHROPIC_API_KEY "sk-ant-api03-YOUR-KEY"
SetEnv GOOGLE_APPLICATION_CREDENTIALS "/absolute/path/to/creds.json"
# Security
<FilesMatch "\.(json|meta)$">
Require all denied
</FilesMatch>
# PHP Settings
php_value upload_max_filesize 50M
php_value post_max_size 50M
php_value max_execution_time 300
```
### Option 2: Enter in Web Interface
The web interface allows you to enter API keys directly on each upload.
### Option 3: PHP Config
Create `config.php`:
```php
<?php
// DO NOT COMMIT THIS FILE
define('ANTHROPIC_API_KEY', 'sk-ant-api03-YOUR-KEY');
define('GOOGLE_APPLICATION_CREDENTIALS', '/absolute/path/to/creds.json');
```
Then in `api.php`:
```php
// At top of file
if (file_exists(__DIR__ . '/config.php')) {
require_once __DIR__ . '/config.php';
}
```
---
## 📦 Complete MAMP Setup Checklist
- [ ] Install system dependencies (Tesseract, Poppler)
- [ ] Create Python venv
- [ ] Install Python packages in venv
- [ ] Configure API keys
- [ ] Copy project to MAMP htdocs
- [ ] Update api.php to use venv Python
- [ ] Create uploads/results/.cache directories
- [ ] Set directory permissions
- [ ] Configure MAMP (PHP 7.4+)
- [ ] Start MAMP servers
- [ ] Test at http://localhost:8888/pdf-checker/
- [ ] Verify branding (black/yellow colors, Montserrat font)
- [ ] Test PDF upload and check
---
## 🎯 Quick Reference
### Activate venv
```bash
source venv/bin/activate
```
### Deactivate venv
```bash
deactivate
```
### Test Python script
```bash
python enterprise_pdf_checker.py test.pdf --output result.json
```
### MAMP URL
```
http://localhost:8888/pdf-checker/
```
### Log files (for debugging)
```bash
# Check Apache error log
tail -f /Applications/MAMP/logs/apache_error.log
# Check PHP error log
tail -f /Applications/MAMP/logs/php_error.log
```
---
## 🌟 Benefits of venv
**Isolated Dependencies** - Won't conflict with system Python
**Clean Uninstall** - Just delete venv folder
**Version Control** - Each project has its own packages
**No sudo Required** - Install packages without admin
**Reproducible** - Same environment everywhere
---
## 💡 Pro Tips
1. **Always activate venv** before running Python scripts
2. **Use absolute paths** in api.php for reliability
3. **Check logs** if something doesn't work
4. **Test Python separately** before testing web interface
5. **Keep API keys in .htaccess** (add to .gitignore)
6. **Use MAMP's PHP** (not system PHP) for consistency
---
## 🎨 Customizing Oliver Branding Further
Want to adjust colors? Edit `index.html`:
```css
:root {
--primary: #FFC407; /* Oliver Yellow */
--black: #000000; /* Oliver Black */
--primary-dark: #e6b006; /* Darker yellow for hover */
/* ... other colors ... */
}
```
Want different fonts? Update the Google Fonts import:
```html
<link href="https://fonts.googleapis.com/css2?family=YourFont:wght@400;600;700&display=swap" rel="stylesheet">
```
---
You're all set! The system is now optimized for:
- ✅ MAMP local development
- ✅ Python venv isolation
- ✅ Oliver branding (Black + Yellow #FFC407)
- ✅ Claude Sonnet 4.5
- ✅ Montserrat font
**Start with:** `source venv/bin/activate` then open http://localhost:8888/pdf-checker/ 🚀

View file

@ -1,323 +0,0 @@
# 🎨 Oliver Customization Summary
## ✅ All Changes Applied
### 🎨 **Branding Updates**
#### Colors
- **Primary**: #FFC407 (Oliver Yellow) ✅
- **Secondary**: #000000 (Black) ✅
- **Previous**: Blue (#2563eb) → Replaced with Yellow/Black
#### Typography
- **Font**: Montserrat (all weights: 400, 600, 700) ✅
- **Loaded from**: Google Fonts CDN
- **Applied to**: Entire application
#### Design Elements
✅ Black header with yellow accent border
✅ Yellow primary buttons with black text
✅ Black/yellow gradient score display
✅ Montserrat font across all text
✅ Yellow hover states
✅ Professional, high-contrast design
---
### 🤖 **AI Model Update**
**Claude Sonnet 4.5** ✅
- Model: `claude-sonnet-4-5-20250929`
- Previous: `claude-3-5-sonnet-20241022`
- **Benefits**: Higher accuracy, better recommendations, improved image analysis
- **Cost**: Same as 3.5 (~$0.015 per image)
---
### 🐍 **Python venv Support**
#### api.php Updates ✅
```php
// Automatically detects and uses venv Python
$venv_python = __DIR__ . '/venv/bin/python3';
$python_bin = file_exists($venv_python) ? $venv_python : 'python3';
```
**What this means:**
- ✅ Works with or without venv
- ✅ No manual configuration needed
- ✅ Falls back to system Python if venv not present
- ✅ MAMP-friendly
---
### 📦 **New Files Added**
1. **MAMP_SETUP.md** (12KB)
- Complete MAMP setup guide
- venv instructions
- Troubleshooting
- Daily workflow
- API key configuration
2. **install_venv.sh** (5.7KB)
- Automated venv setup
- Installs dependencies in venv
- Creates directories
- Tests installation
- Interactive prompts
---
### 🗂️ **File Changes**
#### index.html (25KB) ✅
```html
<!-- Added Montserrat font -->
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
<!-- Updated CSS variables -->
:root {
--primary: #FFC407; /* Oliver Yellow */
--black: #000000; /* Oliver Black */
--primary-dark: #e6b006; /* Darker yellow */
}
<!-- Updated header -->
<header style="background: black; border-bottom: 3px solid yellow;">
```
#### api.php (7.3KB) ✅
```php
// Auto-detect venv Python
$venv_python = __DIR__ . '/venv/bin/python3';
$python_bin = file_exists($venv_python) ? $venv_python : 'python3';
```
#### enterprise_pdf_checker.py (44KB) ✅
```python
# Updated model
model="claude-sonnet-4-5-20250929"
```
---
## 🚀 **Quick Start for MAMP**
### Installation
```bash
# 1. Run venv installer
chmod +x install_venv.sh
./install_venv.sh
# 2. Copy to MAMP (choose one)
# Option A: Copy
cp -r . /Applications/MAMP/htdocs/pdf-checker
# Option B: Symlink
ln -s $(pwd) /Applications/MAMP/htdocs/pdf-checker
# 3. Set API keys
export ANTHROPIC_API_KEY="sk-ant-api03-YOUR-KEY"
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/creds.json"
# 4. Start MAMP and visit
open http://localhost:8888/pdf-checker/
```
### Daily Usage
```bash
# Activate venv (for Python development)
source venv/bin/activate
# Run checks
python enterprise_pdf_checker.py test.pdf
# Deactivate when done
deactivate
```
**For web interface:** Just use MAMP - api.php handles venv automatically! 🎉
---
## 🎯 **What You Get**
### ✅ Oliver Branding
- Black and yellow color scheme
- Montserrat font throughout
- Professional, high-contrast design
- Maintains accessibility while being on-brand
### ✅ Claude Sonnet 4.5
- Latest and most capable model
- Better accuracy for accessibility checks
- Improved recommendations
- Same cost structure
### ✅ venv Support
- Isolated Python environment
- MAMP-compatible
- Automatic detection in api.php
- No manual configuration needed
### ✅ Complete Documentation
- MAMP_SETUP.md - Detailed setup guide
- install_venv.sh - Automated installation
- All original docs still included
- Troubleshooting section
---
## 📊 **Before vs After**
| Feature | Before | After |
|---------|--------|-------|
| **Primary Color** | Blue (#2563eb) | Yellow (#FFC407) ✅ |
| **Secondary Color** | Light Blue | Black (#000000) ✅ |
| **Font** | System default | Montserrat ✅ |
| **AI Model** | Claude 3.5 Sonnet | Claude 4.5 Sonnet ✅ |
| **Python** | System Python | venv support ✅ |
| **MAMP Guide** | Generic setup | Specific MAMP guide ✅ |
---
## 🔍 **Visual Changes**
### Header
```
Before: White background, blue text
After: Black background, yellow text, yellow border
```
### Buttons
```
Before: Blue background, white text
After: Black background, yellow text, yellow border
Hover: Yellow background, black text
```
### Score Display
```
Before: Purple gradient
After: Black gradient with yellow accents
```
### Typography
```
Before: System fonts (-apple-system, etc.)
After: Montserrat for all text
```
---
## 🎨 **Color Palette**
```css
/* Oliver Brand Colors */
--primary: #FFC407; /* Yellow - main brand color */
--primary-dark: #e6b006; /* Darker yellow for hover */
--primary-darker: #cc9d05; /* Even darker for active states */
--black: #000000; /* Black - secondary brand color */
/* Status Colors (unchanged for accessibility) */
--success: #10b981; /* Green */
--warning: #f59e0b; /* Orange */
--error: #ef4444; /* Red */
--critical: #dc2626; /* Dark red */
--info: #3b82f6; /* Blue */
```
---
## 🛠️ **Technical Details**
### Font Loading
```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
```
### venv Detection
```php
// In api.php
$venv_python = __DIR__ . '/venv/bin/python3';
$python_bin = file_exists($venv_python) ? $venv_python : 'python3';
```
### Model Configuration
```python
# In enterprise_pdf_checker.py
self.anthropic_client.messages.create(
model="claude-sonnet-4-5-20250929",
max_tokens=1024,
messages=[...]
)
```
---
## ✅ **Testing Checklist**
Before deploying, verify:
- [ ] Header is black with yellow accent
- [ ] All text uses Montserrat font
- [ ] Primary buttons are black with yellow text
- [ ] Hover states show yellow background
- [ ] Score display has black/yellow gradient
- [ ] Upload area uses appropriate colors
- [ ] API returns Claude Sonnet 4.5 responses
- [ ] venv Python is used when available
- [ ] System Python works as fallback
- [ ] All functionality works in MAMP
---
## 📞 **Need to Customize More?**
### Change Colors
Edit `index.html`, find:
```css
:root {
--primary: #FFC407; /* Change this */
--black: #000000; /* Or this */
}
```
### Change Font
Edit `index.html`, find:
```html
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet">
```
Replace `Montserrat` with your font, then update:
```css
body {
font-family: 'YourFont', sans-serif;
}
```
### Change Model
Edit `enterprise_pdf_checker.py`, find:
```python
model="claude-sonnet-4-5-20250929"
```
---
## 🎉 **Summary**
You now have:
**Oliver-branded** web interface (Black + Yellow #FFC407)
**Montserrat font** throughout
**Claude Sonnet 4.5** integration
**venv support** with automatic detection
**MAMP-optimized** setup
✅ **Complete documentation**
**Everything is ready for MAMP local development!** 🚀
Start with: `./install_venv.sh` then check out **MAMP_SETUP.md**

View file

@ -1,220 +0,0 @@
╔════════════════════════════════════════════════════════════════════════════╗
║ ║
║ 🎯 ENTERPRISE PDF ACCESSIBILITY CHECKER - COMPLETE PACKAGE ║
║ ║
║ The most comprehensive PDF accessibility validation system available ║
║ ║
╚════════════════════════════════════════════════════════════════════════════╝
📦 WHAT YOU HAVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 95% WCAG 2.1 Coverage - Industry-leading automated validation
✅ AI-Powered Analysis - Anthropic Claude 3.5 + Google Cloud Vision
✅ Professional Web Interface - Modern drag-and-drop UI
✅ REST API - Easy integration
✅ Command Line Interface - Automation ready
✅ Complete Documentation - 140KB+ of guides
Total Value: $50,000+ enterprise solution provided complete
🚀 QUICK START (5 MINUTES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Install everything:
$ chmod +x install.sh && ./install.sh
2. Set up API keys (NEW: .env file support!):
$ cp .env.example .env
$ nano .env # Add your API keys here
Or use environment variables:
$ export ANTHROPIC_API_KEY="sk-ant-YOUR-KEY-HERE"
$ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/credentials.json"
3. Quick test (fast mode):
$ python3 enterprise_pdf_checker.py sample_good.pdf --quick
4. Start the server:
$ php -S localhost:8000
5. Open browser:
$ open http://localhost:8000
6. Upload a PDF and get comprehensive accessibility report!
📚 READ THE DOCUMENTATION IN THIS ORDER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🟢 START HERE (Required - 20 minutes)
├─ START_HERE.md .................. Package overview & guide
└─ QUICKSTART.md .................. 5-minute setup instructions
🔵 CORE DOCUMENTATION (Read these next - 1 hour)
├─ ENTERPRISE_README.md ........... Complete installation & usage guide
└─ ARCHITECTURE.md ................ System design & technical details
🟡 BACKGROUND & CONTEXT (Optional - 2 hours)
├─ WCAG_LIMITATIONS.md ............ What can't be automated (5%)
├─ INTEGRATION_GUIDE.md ........... API integration strategies
├─ IMPLEMENTATION_ROADMAP.md ...... Step-by-step coding guide
├─ API_QUICK_REFERENCE.md ......... One-page cheat sheet
└─ MASTER_GUIDE.md ................ Evolution & best practices
📁 FILE STRUCTURE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CORE APPLICATION (Use these):
├── enterprise_pdf_checker.py (44KB) ... Main Python checker with AI
├── api.php (7.1KB) .................... REST API backend
├── index.html (24KB) .................. Modern web interface
├── requirements.txt (480B) ............ Python dependencies
└── install.sh (3.1KB) ................. Automated setup script
DOCUMENTATION (Read these):
├── START_HERE.md (14KB) ............... 👈 Read this first!
├── QUICKSTART.md (9.1KB) .............. Quick setup guide
├── ENTERPRISE_README.md (18KB) ........ Complete documentation
├── ARCHITECTURE.md (17KB) ............. System design
├── WCAG_LIMITATIONS.md (14KB) ......... What can't be automated
├── INTEGRATION_GUIDE.md (25KB) ........ API integration
├── IMPLEMENTATION_ROADMAP.md (25KB) ... Coding guide
├── API_QUICK_REFERENCE.md (11KB) ...... Cheat sheet
└── MASTER_GUIDE.md (12KB) ............. Overview & best practices
TESTING & EXAMPLES:
├── sample_good.pdf (1.4KB) ............ Test PDF with metadata
├── sample_poor.pdf (2.1KB) ............ Test PDF with issues
├── create_sample_pdfs.py (2.7KB) ...... Generate test files
└── accessibility_report.html (6.5KB) .. Example HTML report
LEGACY/ALTERNATIVES (Reference only):
├── pdf_accessibility_checker.py (22KB) .... Basic version (no AI)
├── enhanced_pdf_checker.py (29KB) ......... Intermediate version
└── README.md (9.5KB) ...................... Basic tool docs
💎 KEY FEATURES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚡ Performance & Usability (NEW!)
• Quick mode (--quick) for fast initial checks
• Parallel image processing (3x faster)
• Smart API timeouts (no more hangs!)
• .env file support for secure API keys
• Real-time progress updates
🤖 AI-Powered Analysis
• Claude 3.5 Sonnet for image analysis (95% accuracy)
• Google Cloud Vision for OCR (98% accuracy)
• Alt text quality validation
• Text-in-images detection
• Content quality analysis
🔍 Comprehensive WCAG Checks
• Document structure & tagging (1.3.1, 4.1.2)
• Color contrast analysis (1.4.3)
• Text extractability & readability (3.1.5)
• Form field validation (3.3.2)
• Link quality checking (2.4.4)
• 30+ automated checks total
🌐 Three Usage Modes
• Web Interface: Drag-and-drop with visual reports
• Command Line: Automation & batch processing
• REST API: System integration
💰 Cost-Effective
• ~$0.10 per document (10 pages, 5 images)
• Smart caching reduces repeat checks to $0
• Break-even after 2-3 documents vs manual review
💰 COSTS & ROI
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Per Document: ~$0.10 (Anthropic $0.075 + Google $0.008 + OCR $0.015)
Monthly Costs:
• 100 documents .... $10/month
• 500 documents .... $50/month
• 1,000 documents .. $100/month
• 5,000 documents .. $500/month
ROI:
• Manual review: $100/document (2 hours @ $50/hr)
• This tool: $0.10/document (2 minutes)
• Savings: $99.90 per document
• Break-even: After 2-3 documents
• Time savings: 96% reduction
🎯 COMPARISON WITH ALTERNATIVES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
This Tool Adobe Acrobat PAC (Free) Manual Review
Coverage 95% 90% 75% 100%
Speed 2-5 min 5-10 min 3-5 min 1-2 hours
AI Analysis Yes No No Yes
Automation Full Limited Limited No
API Access Yes No No No
Cost/Document $0.10 $20+ $0 $100
Quality Rating ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
🔒 SECURITY & COMPLIANCE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ WCAG 2.1 Level A & AA compliant
✅ PDF/UA standards aligned
✅ Section 508 compatible
✅ EN 301 549 aligned
✅ HTTPS required for production
✅ API keys in environment variables
✅ No data retention policies configurable
✅ File upload validation & size limits
📞 GETTING HELP
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Check START_HERE.md for overview
2. Read QUICKSTART.md for setup
3. See ENTERPRISE_README.md for troubleshooting
4. Review ARCHITECTURE.md for technical details
5. All API documentation included
✨ WHAT MAKES THIS SPECIAL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Quality-First Design - Uses best AI models (Claude, Google)
✓ Production-Ready - Enterprise-grade code & architecture
✓ Complete Package - Nothing else to buy or build
✓ Well-Documented - 140KB+ of guides & examples
✓ Cost-Optimized - Smart caching & efficient processing
✓ Three Interfaces - Web, CLI, and API
✓ Easy Integration - REST API for existing systems
✓ Proven Technology - Built on industry-standard libraries
🎯 NEXT STEPS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. NOW: Read START_HERE.md (5 minutes)
2. TODAY: Run ./install.sh and configure API keys
3. THIS WEEK: Test with 10-20 documents
4. THIS MONTH: Deploy to production
5. THIS QUARTER: Achieve 95% WCAG coverage goal
═══════════════════════════════════════════════════════════════════════════════
🌟 Make the web accessible for everyone 🌟
Start with START_HERE.md →
═══════════════════════════════════════════════════════════════════════════════

View file

@ -1,143 +0,0 @@
╔════════════════════════════════════════════════════════════════════╗
║ ║
║ 🎨 OLIVER ENTERPRISE PDF ACCESSIBILITY CHECKER ║
║ ║
║ Customized with Oliver branding + MAMP + venv support ║
║ ║
╚════════════════════════════════════════════════════════════════════╝
📚 READ IN THIS ORDER FOR MAMP SETUP:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1⃣ OLIVER_CUSTOMIZATION.md ............... What changed (5 min)
↓ Summary of all Oliver-specific updates
2⃣ MAMP_SETUP.md .......................... MAMP setup guide (15 min)
↓ Step-by-step MAMP configuration
3⃣ Run: ./install_venv.sh ................ Auto-install (5 min)
↓ Creates venv and installs everything
4⃣ START_HERE.md .......................... Full package overview
↓ Complete system documentation
🚀 SUPER QUICK START (10 MINUTES):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
$ ./install_venv.sh
$ export ANTHROPIC_API_KEY="sk-ant-YOUR-KEY"
$ export GOOGLE_APPLICATION_CREDENTIALS="/path/to/creds.json"
Then copy to MAMP:
$ cp -r . /Applications/MAMP/htdocs/pdf-checker
Open: http://localhost:8888/pdf-checker/
Done! 🎉
✨ WHAT'S CUSTOMIZED:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Oliver Colors: Black (#000000) + Yellow (#FFC407)
✅ Oliver Font: Montserrat (all weights)
✅ Latest AI: Claude Sonnet 4.5
✅ venv Support: Automatic detection in api.php
✅ MAMP Ready: No port conflicts, works out of the box
📁 KEY FILES:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SETUP & DOCUMENTATION:
├── OLIVER_CUSTOMIZATION.md ......... What changed for Oliver
├── MAMP_SETUP.md ................... Complete MAMP guide
├── install_venv.sh ................. Auto-installer
└── START_HERE.md ................... Full documentation
APPLICATION (UPDATED):
├── index.html ...................... Oliver branding applied
├── api.php ......................... venv auto-detection
├── enterprise_pdf_checker.py ....... Claude Sonnet 4.5
└── requirements.txt ................ All dependencies
REFERENCE:
├── ENTERPRISE_README.md ............ Complete manual
├── ARCHITECTURE.md ................. System design
├── QUICKSTART.md ................... 5-min generic setup
└── [8 more documentation files]
🎨 OLIVER BRANDING DETAILS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Primary Color: #FFC407 (Yellow)
Secondary Color: #000000 (Black)
Font: Montserrat (400, 600, 700)
Visual Elements:
• Black header with yellow border
• Yellow primary buttons
• Black/yellow score display
• High-contrast, professional design
• Fully accessible while on-brand
🤖 AI CONFIGURATION:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Model: Claude Sonnet 4.5 (claude-sonnet-4-5-20250929)
Why: Latest model, highest accuracy
Cost: ~$0.015 per image (same as 3.5)
Bonus: Also uses Google Cloud Vision for cross-validation
🐍 PYTHON VENV:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Isolated environment (no conflicts)
✅ Auto-detected by api.php
✅ Falls back to system Python if needed
✅ Easy to manage
Activate: source venv/bin/activate
Deactivate: deactivate
Run: python enterprise_pdf_checker.py file.pdf
💡 COMMON TASKS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Test Python script:
$ source venv/bin/activate
$ python enterprise_pdf_checker.py sample.pdf
$ deactivate
Use web interface:
Just open: http://localhost:8888/pdf-checker/
(api.php handles venv automatically)
Add to MAMP:
$ cp -r . /Applications/MAMP/htdocs/pdf-checker
OR
$ ln -s $(pwd) /Applications/MAMP/htdocs/pdf-checker
🎯 NEXT STEPS:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Read OLIVER_CUSTOMIZATION.md to see what changed
2. Read MAMP_SETUP.md for detailed instructions
3. Run ./install_venv.sh to set up venv
4. Set your API keys
5. Add to MAMP htdocs
6. Visit http://localhost:8888/pdf-checker/
7. Upload a PDF and test!
═══════════════════════════════════════════════════════════════════════
🎨 Oliver-branded, Claude 4.5-powered, venv-ready! 🚀
═══════════════════════════════════════════════════════════════════════

View file

@ -1,118 +0,0 @@
#!/bin/bash
# Enterprise PDF Accessibility Checker - Installation Script
set -e
echo "=========================================="
echo "Enterprise PDF Accessibility Checker"
echo "Installation Script"
echo "=========================================="
echo ""
# Check if running as root
if [ "$EUID" -eq 0 ]; then
echo "Please do not run as root/sudo"
exit 1
fi
# Detect OS
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="linux"
PKG_MGR="apt-get"
elif [[ "$OSTYPE" == "darwin"* ]]; then
OS="mac"
PKG_MGR="brew"
else
echo "Unsupported OS: $OSTYPE"
exit 1
fi
echo "Detected OS: $OS"
echo ""
# Step 1: Install system dependencies
echo "Step 1: Installing system dependencies..."
if [ "$OS" == "linux" ]; then
sudo apt-get update
sudo apt-get install -y \
python3 \
python3-pip \
tesseract-ocr \
poppler-utils \
php \
php-cli \
php-json
elif [ "$OS" == "mac" ]; then
brew install python3 tesseract poppler php
fi
echo "✓ System dependencies installed"
echo ""
# Step 2: Install Python dependencies
echo "Step 2: Installing Python dependencies..."
pip3 install -r requirements.txt --break-system-packages || pip3 install -r requirements.txt
echo "✓ Python dependencies installed"
echo ""
# Step 3: Download TextBlob corpora
echo "Step 3: Downloading TextBlob language data..."
python3 -m textblob.download_corpora lite
echo "✓ TextBlob corpora downloaded"
echo ""
# Step 4: Create required directories
echo "Step 4: Creating directories..."
mkdir -p uploads results .cache
chmod 755 uploads results .cache
echo "✓ Directories created"
echo ""
# Step 5: Test installation
echo "Step 5: Testing installation..."
python3 enterprise_pdf_checker.py --help > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✓ Installation successful!"
else
echo "⚠ Warning: Python script test failed"
fi
echo ""
# Step 6: Check for API keys
echo "Step 6: Checking API configuration..."
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "⚠ ANTHROPIC_API_KEY not set"
echo " Export it with: export ANTHROPIC_API_KEY='sk-ant-...'"
else
echo "✓ Anthropic API key found"
fi
if [ -z "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
echo "⚠ GOOGLE_APPLICATION_CREDENTIALS not set"
echo " Export it with: export GOOGLE_APPLICATION_CREDENTIALS='/path/to/creds.json'"
else
echo "✓ Google credentials found"
fi
echo ""
# Final instructions
echo "=========================================="
echo "Installation Complete!"
echo "=========================================="
echo ""
echo "Next steps:"
echo ""
echo "1. Configure API keys (if not already done):"
echo " export ANTHROPIC_API_KEY='sk-ant-...'"
echo " export GOOGLE_APPLICATION_CREDENTIALS='/path/to/creds.json'"
echo ""
echo "2. Start the web server:"
echo " php -S localhost:8000"
echo ""
echo "3. Open in browser:"
echo " http://localhost:8000"
echo ""
echo "Or use the command line:"
echo " python3 enterprise_pdf_checker.py your_document.pdf"
echo ""
echo "See ENTERPRISE_README.md for detailed documentation."
echo ""

View file

@ -1,186 +0,0 @@
#!/bin/bash
# Enterprise PDF Accessibility Checker - venv Installation Script
# For use with MAMP or local development
set -e
echo "=========================================="
echo "Enterprise PDF Accessibility Checker"
echo "MAMP + venv Installation"
echo "=========================================="
echo ""
# Detect OS
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
OS="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
OS="mac"
else
echo "Unsupported OS: $OSTYPE"
exit 1
fi
echo "Detected OS: $OS"
echo ""
# Step 1: Check for Python 3
echo "Step 1: Checking Python installation..."
if command -v python3 &> /dev/null; then
PYTHON_VERSION=$(python3 --version)
echo "$PYTHON_VERSION found"
else
echo "✗ Python 3 not found"
echo "Please install Python 3.8 or higher first:"
if [ "$OS" == "mac" ]; then
echo " brew install python3"
else
echo " sudo apt-get install python3 python3-pip python3-venv"
fi
exit 1
fi
echo ""
# Step 2: Install system dependencies (optional, with user confirmation)
echo "Step 2: System dependencies (Tesseract, Poppler)..."
echo "These are required for OCR and PDF rendering."
read -p "Install system dependencies? (y/n) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
if [ "$OS" == "linux" ]; then
sudo apt-get update
sudo apt-get install -y tesseract-ocr poppler-utils
elif [ "$OS" == "mac" ]; then
brew install tesseract poppler
fi
echo "✓ System dependencies installed"
else
echo "⚠ Skipped system dependencies. Install manually if needed."
fi
echo ""
# Step 3: Create virtual environment
echo "Step 3: Creating Python virtual environment..."
if [ -d "venv" ]; then
echo "⚠ venv directory already exists"
read -p "Delete and recreate? (y/n) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
rm -rf venv
else
echo "Keeping existing venv"
fi
fi
if [ ! -d "venv" ]; then
python3 -m venv venv
echo "✓ Virtual environment created"
else
echo "✓ Using existing virtual environment"
fi
echo ""
# Step 4: Activate venv and install dependencies
echo "Step 4: Installing Python dependencies in venv..."
source venv/bin/activate
# Upgrade pip
pip install --upgrade pip --quiet
# Install dependencies
pip install -r requirements.txt --quiet
echo "✓ Python dependencies installed in venv"
echo ""
# Step 5: Download TextBlob corpora
echo "Step 5: Downloading TextBlob language data..."
python -m textblob.download_corpora lite 2>/dev/null || echo "⚠ TextBlob corpora download skipped"
echo ""
# Step 6: Create required directories
echo "Step 6: Creating directories..."
mkdir -p uploads results .cache
chmod 755 uploads results .cache
echo "✓ Directories created"
echo ""
# Step 7: Test installation
echo "Step 7: Testing installation..."
python enterprise_pdf_checker.py --help > /dev/null 2>&1
if [ $? -eq 0 ]; then
echo "✓ Python script test passed"
else
echo "⚠ Warning: Python script test failed"
fi
echo ""
# Step 8: Check for API keys
echo "Step 8: Checking API configuration..."
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "⚠ ANTHROPIC_API_KEY not set"
echo ""
echo "Set it now:"
echo " export ANTHROPIC_API_KEY='sk-ant-api03-...'"
echo ""
echo "Or add to shell profile (~/.zshrc or ~/.bashrc):"
echo " echo 'export ANTHROPIC_API_KEY=\"sk-ant-api03-...\"' >> ~/.zshrc"
else
echo "✓ Anthropic API key found"
fi
if [ -z "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
echo "⚠ GOOGLE_APPLICATION_CREDENTIALS not set"
echo ""
echo "Set it now:"
echo " export GOOGLE_APPLICATION_CREDENTIALS='/absolute/path/to/credentials.json'"
echo ""
echo "Or add to shell profile:"
echo " echo 'export GOOGLE_APPLICATION_CREDENTIALS=\"/path/to/creds.json\"' >> ~/.zshrc"
else
echo "✓ Google credentials found"
fi
echo ""
# Deactivate venv
deactivate
# Final instructions
echo "=========================================="
echo "Installation Complete!"
echo "=========================================="
echo ""
echo "✅ Virtual environment created at: ./venv"
echo "✅ All dependencies installed"
echo "✅ Claude Sonnet 4.5 configured"
echo "✅ Oliver branding applied (Black + Yellow #FFC407)"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Next Steps:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "1. Configure API keys (if not already done):"
echo " export ANTHROPIC_API_KEY='sk-ant-api03-...'"
echo " export GOOGLE_APPLICATION_CREDENTIALS='/path/to/creds.json'"
echo ""
echo "2. For MAMP setup:"
echo " - Copy this folder to MAMP htdocs/"
echo " - Or create symlink: ln -s $(pwd) /Applications/MAMP/htdocs/pdf-checker"
echo " - Start MAMP and visit: http://localhost:8888/pdf-checker/"
echo ""
echo "3. To use command line:"
echo " source venv/bin/activate"
echo " python enterprise_pdf_checker.py your_document.pdf"
echo " deactivate"
echo ""
echo "4. Read MAMP_SETUP.md for detailed MAMP configuration"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Daily Usage:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Activate venv: source venv/bin/activate"
echo "Deactivate venv: deactivate"
echo "Run checker: python enterprise_pdf_checker.py file.pdf"
echo ""
echo "The api.php automatically detects and uses venv Python! 🎉"
echo ""

793
README.md
View file

@ -1,774 +1,51 @@
# PDF Accessibility Checker - Current State
# Aimpress PDF Accessibility
> **AI-Powered PDF Accessibility Validation System**
> Comprehensive WCAG 2.1 compliance checking with enterprise-grade features
> WCAG 2.1 AA compliance checking for PDFs — web-based, AI-powered, self-service.
---
**EU Accessibility Act (June 2025)** requires banks, e-commerce, e-learning, and government to provide accessible PDF documents. This product automates the audit process.
## 📋 What This Application Does
## Features
This is a **production-ready PDF accessibility checker** that validates PDF documents against WCAG 2.1 Level A & AA standards. It combines traditional PDF analysis with cutting-edge AI to achieve approximately **95% automated coverage** of accessibility requirements.
- 30+ WCAG 2.1 AA / PDF/UA-1 checks
- AI-powered alt-text validation (Claude Sonnet + Google Vision)
- Color contrast checking (1.4.3 AA + 1.4.6 AAA)
- Auto-remediation (title, language, tags, bookmarks)
- Visual Page Inspector — SVG overlay of issues on rendered pages
- Multi-language support (50+ languages)
- Detailed HTML/JSON/PDF reports with Matterhorn Protocol checkpoints
- Team workspaces with role-based access
### 🆕 Recent Updates (Feb 2026)
## Tech Stack
**Production Readiness Enhancements:**
- ✅ **API Authentication** - Secure API access with key-based authentication
- ✅ **Structured Logging** - Production-grade logging with rotation and levels
- ✅ **Error Resilience** - Automatic retry logic with exponential backoff for API calls
- ✅ **Test Suite** - 31 automated tests ensuring code quality (34% coverage)
- ✅ **veraPDF Integration** - Enhanced PDF/UA-1 validation (ISO 14289-1)
- ✅ **Virtual Environment** - Isolated Python dependencies for clean deployment
- ✅ **Requirements Docs** - Full BRS/FRS/SAD specifications in `docs_req/`
- ✅ **Bug Fixes** - Critical import bug fixed in remediation module
| Layer | Technology |
|---|---|
| Backend | FastAPI + Python 3.12 |
| Frontend | Next.js 15 + shadcn/ui + Tailwind |
| Auth | Supabase Auth |
| Database | PostgreSQL 16 + RLS |
| Queue | Celery + Redis |
| Storage | MinIO (S3-compatible) |
| Deploy | Docker Compose + Caddy |
**Status:** 95% Production-Ready • All Critical Fixes Complete • All Tests Passing
### Core Capabilities
**Automated WCAG Validation** - Checks 30+ accessibility criteria
**AI-Powered Image Analysis** - Uses Anthropic Claude 3.5 Sonnet for alt text validation
**OCR & Text Detection** - Google Cloud Vision for text-in-images detection
**Color Contrast Analysis** - WCAG AA/AAA compliance checking
**Readability Metrics** - Flesch scores and grade-level analysis
**Auto-Remediation** - Fixes common issues automatically
**Visual Inspector** - See exactly where issues occur on each page
**Three Interfaces** - Web UI, REST API, and Command Line
**API Authentication** - Secure API access with key-based authentication
**Structured Logging** - Production-ready logging with rotation
**Error Resilience** - Automatic retry logic for API failures
**Test Suite** - 31 automated tests with 34% coverage
**veraPDF Integration** - Enhanced PDF/UA compliance validation
---
## 🏗️ System Architecture
### Components
```
┌─────────────────────────────────────────────────────┐
│ Web Interface (index.html) │
│ • Drag-and-drop PDF upload │
│ • Real-time progress tracking │
│ • Visual results dashboard │
│ • Issue filtering and navigation │
└──────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ REST API (api.php) │
│ • File upload management │
│ • Job queue processing │
│ • Result storage and retrieval │
│ • Auto-remediation endpoint │
└──────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ Processing Engine (enterprise_pdf_checker.py) │
│ • PDF structure analysis │
│ • Image extraction and AI analysis │
│ • Color contrast checking │
│ • Readability analysis │
│ • Comprehensive reporting │
└─────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────┐
│ External APIs │ │ Remediation Engine │
│ • Claude Vision │ │ (pdf_remediation.py) │
│ • Google Vision │ │ • Metadata fixes │
│ • Document AI │ │ • Language setting │
└──────────────────┘ │ • Tagging corrections │
└──────────────────────────┘
```
### File Structure
```
PDF-Accessibility-checker/
├── enterprise_pdf_checker.py # Main checker (1,508 lines)
├── pdf_remediation.py # Auto-fix engine (455 lines)
├── api.php # REST API backend (532 lines)
├── index.html # Web interface (1,727 lines)
├── auth.php # Authentication module (NEW)
├── logger_config.py # Logging framework (NEW)
├── retry_helper.py # API retry logic (NEW)
├── requirements.txt # Python dependencies
├── pytest.ini # Test configuration (NEW)
├── .env.example # Environment configuration template
├── venv/ # Virtual environment (created during setup)
├── uploads/ # Uploaded PDFs (temporary)
├── results/ # Check results and metadata
├── .cache/ # API response cache (cost optimization)
├── logs/ # Application logs (NEW)
├── tests/ # Test suite (NEW)
│ ├── conftest.py # pytest fixtures
│ ├── test_checker.py # Checker unit tests
│ ├── test_remediation.py # Remediation tests
│ └── test_api.py # API integration tests
├── Test_files/ # Sample PDFs for testing
│ ├── sample_good.pdf
│ └── sample_poor.pdf
├── docs_req/ # Requirements specifications (NEW)
│ ├── PDFAccessibilityHub_BRS_v1.1_2026-02-02.md
│ ├── PDFAccessibilityHub_FRS_v1.1_2026-02-02.md
│ └── PDFAccessibilityHub_SAD_v1.1_2026-02-02.md
└── README's/ # Extensive documentation (19 files)
├── START_HERE.md
├── QUICKSTART.md
├── ENTERPRISE_README.md
├── ARCHITECTURE.md
├── WCAG_LIMITATIONS.md
└── ... (14 more guides)
```
---
## 🚀 Quick Setup Guide
### Prerequisites
- **Python 3.8+**
- **PHP 7.4+** (for web interface)
- **Tesseract OCR** (for text extraction)
- **Poppler** (for PDF rendering)
- **API Keys:**
- Anthropic API key (required for AI analysis)
- Google Cloud credentials (optional, enhances analysis)
### Installation (10 Minutes)
## Local Development
```bash
# 1. Navigate to project directory
cd /path/to/PDF-Accessibility-checker
# 2. Create virtual environment (recommended)
python3 -m venv venv
source venv/bin/activate
# 3. Install Python dependencies
pip install -r requirements.txt
# 4. Install system dependencies (macOS)
brew install php tesseract poppler
# Optional: Install veraPDF for enhanced PDF/UA validation
brew install verapdf
# 5. Configure API keys
cp .env.example .env
nano .env # Add your Anthropic API key
# Fill in ANTHROPIC_API_KEY + SUPABASE_* values
# 6. Start the web server
php -S localhost:8000
# 7. Open browser
open http://localhost:8000
docker compose up -d postgres redis minio
cd backend && uv sync && uv run uvicorn app.main:app --reload
cd frontend && npm install && npm run dev
```
**Note:** On macOS, use virtual environment to avoid `externally-managed-environment` errors.
## Pricing
### Alternative: Command Line Usage
| Plan | PDFs/month | Auto-fix | API | Team |
|---|---|---|---|---|
| Free | 5 | — | — | — |
| Pro $29/mo | 100 | ✓ | — | — |
| Business $149/mo | Unlimited | ✓ | ✓ | ✓ |
```bash
# Basic check
python3 enterprise_pdf_checker.py document.pdf
## Deployment
# With output file
python3 enterprise_pdf_checker.py document.pdf --output report.json
# Quick mode (skip AI analysis)
python3 enterprise_pdf_checker.py document.pdf --quick
```
---
## 🎯 Key Features Explained
### 1. **AI-Powered Image Analysis**
Uses **Anthropic Claude 3.5 Sonnet** to analyze every image in the PDF:
- Validates alt text quality and meaningfulness
- Detects text embedded in images (WCAG 1.4.5 violation)
- Identifies color-only information (WCAG 1.4.1)
- Classifies images as decorative vs. informational
- Provides specific accessibility recommendations
**Cost:** ~$0.015 per image (cached for free on repeat checks)
### 2. **Comprehensive WCAG Checks**
Automated validation of 30+ criteria including:
- ✅ Document structure and tagging (1.3.1, 4.1.2)
- ✅ Text alternatives for images (1.1.1)
- ✅ Color contrast ratios (1.4.3) - AA/AAA levels
- ✅ Language declaration (3.1.1)
- ✅ Page titles (2.4.2)
- ✅ Link text quality (2.4.4)
- ✅ Form field labels (3.3.2)
- ✅ Reading order (1.3.2)
- ✅ Font embedding (1.4.4)
- ✅ Content readability (3.1.5)
### 3. **Auto-Remediation**
Automatically fixes common issues:
- Missing document title
- Missing author/subject metadata
- Language not set
- Document not marked as tagged
- Missing bookmarks
**Usage:**
```bash
python3 pdf_remediation.py document.pdf --output fixed.pdf --all
```
### 4. **Visual Page Inspector**
- Displays PDF pages as images
- Highlights issue locations with color-coded markers
- Zoom and pan functionality
- Click issues to see exact page location
- Severity-based color coding (Critical/Error/Warning/Info)
### 5. **Smart Caching**
- Caches all API responses by content hash
- Repeat checks of same document = $0 cost
- Similar images across documents = cached automatically
- Reduces typical document cost from $0.10 to $0.00 on re-check
---
## 📊 What Gets Checked
### Fully Automated (75% of WCAG)
| Check | WCAG Criterion | Description |
|-------|----------------|-------------|
| Document Structure | 1.3.1, 4.1.2 | PDF tagging and semantic structure |
| Metadata | 2.4.2, 3.1.1 | Title, language, author, subject |
| Text Extractability | - | Ensures text can be read by screen readers |
| Font Embedding | 1.4.4 | Fonts are embedded for consistent rendering |
| Color Contrast | 1.4.3 | WCAG AA/AAA compliance (4.5:1, 7:1 ratios) |
| Form Fields | 3.3.2 | Labels and descriptions present |
| Links | 2.4.4 | Descriptive link text (not "click here") |
| Reading Order | 1.3.2 | Logical content sequence |
### AI-Assisted (20% of WCAG)
| Check | WCAG Criterion | AI Model | Description |
|-------|----------------|----------|-------------|
| Alt Text Quality | 1.1.1 | Claude 3.5 | Validates meaningfulness of alt text |
| Text in Images | 1.4.5 | Claude + Google Vision | Detects text embedded in images |
| Color-Only Info | 1.4.1 | Claude 3.5 | Identifies information conveyed by color alone |
| Content Readability | 3.1.5 | TextBlob | Flesch scores, grade level analysis |
| Image Classification | 1.1.1 | Claude 3.5 | Decorative vs. informational |
### Requires Manual Review (5% of WCAG)
- ⚠️ Keyboard navigation and tab order (2.1.1)
- ⚠️ Focus indicators (2.4.7)
- ⚠️ Actual screen reader testing
- ⚠️ Semantic structure quality
- ⚠️ Real user experience validation
---
## 💰 Cost Structure
### Per Document Estimate (10 pages, 5 images)
| Service | Usage | Cost |
|---------|-------|------|
| Anthropic Claude | 5 images @ $0.015 | $0.075 |
| Google Cloud Vision | 5 images @ $0.0015 | $0.008 |
| Google Document AI (OCR) | 10 pages @ $0.0015 | $0.015 |
| **Total** | | **~$0.10** |
### Monthly Costs by Volume
- 100 documents/month = **$10**
- 500 documents/month = **$50**
- 1,000 documents/month = **$100**
- 5,000 documents/month = **$500**
### ROI Comparison
| Method | Cost/Document | Time | Coverage |
|--------|---------------|------|----------|
| **This Tool** | $0.10 | 2-5 min | 95% |
| Manual Review | $100 | 1-2 hours | 100% |
| Adobe Acrobat Pro | $20+ | 5-10 min | 90% |
| PAC (Free) | $0 | 3-5 min | 75% |
**Break-even:** After 2-3 documents vs. manual review
**Time savings:** 96% reduction in review time
---
## 🔧 Current Limitations
### What This Tool CANNOT Do
1. **Full Screen Reader Simulation** - Cannot replicate NVDA/JAWS behavior
2. **Keyboard Navigation Testing** - Cannot test actual tab order functionality
3. **Real User Testing** - Cannot replace human accessibility auditors
4. **PDF Creation** - Only validates, doesn't create accessible PDFs
5. **Complex Table Analysis** - Limited validation of table structure complexity
6. **Mathematical Content** - Cannot validate MathML or equation accessibility
### Known Issues
- **Large PDFs (>50MB)** - May timeout or require increased PHP limits
- **Scanned PDFs** - OCR quality depends on scan quality
- **Complex Layouts** - Multi-column layouts may have reading order issues
- **Non-English Content** - AI analysis optimized for English
- **Password-Protected PDFs** - Cannot analyze encrypted documents
---
## 📈 Accessibility Score Calculation
```
Starting Score: 100 points
Deductions:
- Critical Issue: -25 points each
- Error: -10 points each
- Warning: -5 points each
- Info: -2 points each
Minimum Score: 0
```
### Score Interpretation
| Score | Grade | Meaning |
|-------|-------|---------|
| 90-100 | A | Excellent - Minor improvements only |
| 80-89 | B | Good - Several issues to address |
| 70-79 | C | Fair - Significant barriers present |
| 60-69 | D | Poor - Major accessibility issues |
| 0-59 | F | Critical - Document largely inaccessible |
---
## 🔌 API Endpoints
### Authentication
**Development Mode:** Localhost requests (`http://localhost:8000`) do not require authentication.
**Production Mode:** All API requests require authentication via API key.
**Methods:**
```bash
# 1. X-API-Key header (recommended)
curl -H 'X-API-Key: your-api-key' http://your-server.com/api.php
# 2. Authorization Bearer token
curl -H 'Authorization: Bearer your-api-key' http://your-server.com/api.php
# 3. Query parameter (development only)
curl 'http://localhost:8000/api.php?api_key=dev_key_12345'
```
**Generate API Key:**
```bash
curl 'http://localhost:8000/auth.php?generate'
# Returns: b85091698668907e360223e68868fa0a26dd48a2e3500a4eb48200bad63012c6
```
**Default Dev Key:** `dev_key_12345`
---
### Upload PDF
```http
POST /api.php?action=upload
Content-Type: multipart/form-data
X-API-Key: your-api-key
Body: pdf (file)
Response:
{
"success": true,
"data": {
"job_id": "pdf_123456",
"filename": "document.pdf"
}
}
```
### Start Check
```http
POST /api.php?action=check
Content-Type: application/json
Body:
{
"job_id": "pdf_123456",
"quick_mode": false
}
Response:
{
"success": true,
"data": {
"job_id": "pdf_123456",
"status": "processing"
}
}
```
### Get Results
```http
GET /api.php?action=result&job_id=pdf_123456
Response:
{
"success": true,
"data": {
"filename": "document.pdf",
"accessibility_score": 75,
"severity_counts": {...},
"issues": [...]
}
}
```
### Auto-Remediate
```http
POST /api.php?action=remediate
Content-Type: application/json
Body: {"job_id": "pdf_123456"}
Response:
{
"success": true,
"data": {
"remediated_pdf": "pdf_123456_remediated.pdf",
"fixes_applied": 5,
"download_url": "api.php?action=download&job_id=pdf_123456&type=remediated"
}
}
```
---
## 🧪 Testing
### Test Files Included
- `Test_files/sample_good.pdf` - Well-structured PDF with metadata
- `Test_files/sample_poor.pdf` - PDF with multiple accessibility issues
### Quick Test
```bash
# Activate virtual environment
source venv/bin/activate
# Test the checker
python enterprise_pdf_checker.py Test_files/sample_poor.pdf --output test_result.json
# View results
cat test_result.json | python -m json.tool
# Test remediation
python pdf_remediation.py Test_files/sample_poor.pdf --all
```
### Running Automated Tests
```bash
# Activate virtual environment
source venv/bin/activate
# Run all tests
pytest tests/ -v
# Run with coverage report
pytest tests/ --cov=. --cov-report=html
# Run only unit tests (skip integration)
pytest tests/ -m "not integration"
# View coverage report
open htmlcov/index.html
```
**Test Results:**
- ✅ 31 tests passing
- ✅ 34% code coverage
- ✅ Unit tests for checker and remediation
- ✅ Integration tests for API and authentication
---
## 🏭 Production Features
### Authentication & Security
The application now includes production-ready security features:
**API Authentication** ([auth.php](auth.php))
- API key-based authentication for all endpoints
- Support for multiple authentication methods (Bearer token, X-API-Key header, query parameter)
- Development mode bypass for localhost testing
- API key generation utility
**Configuration:**
```bash
# Generate production API key
curl 'http://localhost:8000/auth.php?generate'
# Add to .api_keys file
echo "your-generated-key-here" >> .api_keys
# Or set environment variable
export API_KEY="your-generated-key-here"
```
### Logging & Monitoring
**Structured Logging** ([logger_config.py](logger_config.py))
- Automatic log rotation (10MB max size, 5 backups)
- Multiple log levels (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- Separate logs for different modules
- Logs stored in `logs/` directory
**Log Files:**
- `logs/pdf_checker.log` - Main checker operations
- `logs/pdf_remediation.log` - Remediation operations
- `logs/retry_helper.log` - API retry events
- `logs/php_server.log` - Web server access logs
### Error Resilience
**Automatic Retry Logic** ([retry_helper.py](retry_helper.py))
- Exponential backoff for API failures (1s → 2s → 4s delays)
- Configurable retry attempts (default: 3)
- Graceful degradation on persistent failures
- Applied to all AI API calls (Claude and Google Vision)
**Benefits:**
- Handles transient network failures automatically
- Prevents job failures due to temporary API issues
- Improves overall system reliability
### Testing & Quality Assurance
**Automated Test Suite** ([tests/](tests/))
- 31 unit and integration tests
- 34% code coverage of critical paths
- pytest configuration with coverage reporting
- Tests for checker, remediation, API, and authentication
**Run Tests:**
```bash
source venv/bin/activate
pytest tests/ -v --cov=. --cov-report=html
open htmlcov/index.html
```
### veraPDF Integration
**Enhanced PDF/UA Validation:**
```bash
# Validate PDF/UA-1 compliance
verapdf --defaultflavour ua1 document.pdf
# The remediation module automatically uses veraPDF if installed
```
---
## 📚 Documentation
The `README's/` folder contains **19 comprehensive guides** (140KB+ of documentation):
### Essential Reading
1. **START_HERE.md** - Package overview and quick start
2. **QUICKSTART.md** - 5-minute setup guide
3. **ENTERPRISE_README.md** - Complete installation and usage
4. **ARCHITECTURE.md** - System design and technical details
### Advanced Topics
5. **WCAG_LIMITATIONS.md** - What can't be automated
6. **INTEGRATION_GUIDE.md** - API integration strategies
7. **IMPLEMENTATION_ROADMAP.md** - Step-by-step coding guide
8. **API_QUICK_REFERENCE.md** - One-page cheat sheet
9. **MASTER_GUIDE.md** - Evolution and best practices
### Specialized Guides
- MAMP_SETUP.md - Local server configuration
- PROGRESS_DISPLAY_GUIDE.md - Real-time progress implementation
- TECHNICAL_BACKGROUND.md - Deep dive into accessibility standards
- screen_reader_simulator_proposal.md - Future enhancement ideas
---
## 🔒 Security Considerations
### Current Implementation
✅ File type validation (PDF only)
✅ File size limits (50MB default)
✅ API keys in environment variables
✅ Temporary file cleanup
✅ CORS headers configured
✅ Input sanitization in API
**API Authentication** - API key-based access control
**Development Mode** - Localhost bypass for local testing
**Structured Logging** - Audit trail for all operations
**Error Handling** - Retry logic for API failures
### Production Recommendations
- [ ] Enable HTTPS (required)
- [ ] Implement rate limiting (infrastructure ready in auth.php)
- [x] Add API authentication (✅ Implemented)
- [ ] Set up malware scanning
- [ ] Configure file retention policies
- [x] Enable audit logging (✅ Implemented with logger_config.py)
- [ ] Implement API key rotation
- [ ] Deploy to production server (Apache/Nginx + PHP-FPM)
- [ ] Configure production API keys (replace dev_key_12345)
---
## 🎯 Use Cases
### 1. **Content Publishing**
Check PDFs before publication to ensure accessibility compliance
### 2. **Legal Compliance**
Validate documents meet Section 508, ADA, WCAG 2.1 requirements
### 3. **Quality Assurance**
Integrate into CI/CD pipeline for automated accessibility testing
### 4. **Batch Processing**
Audit large document libraries for accessibility issues
### 5. **Remediation Workflow**
Identify issues → Auto-fix simple problems → Manual review complex cases
---
## 🛠️ Technology Stack
### Backend
- **Python 3.8+** - Core processing engine
- **PHP 7.4+** - REST API and web server
- **Tesseract OCR** - Text extraction from images
- **Poppler** - PDF rendering and conversion
### Python Libraries
- `pypdf` - PDF parsing and manipulation
- `pdfplumber` - Advanced PDF analysis
- `Pillow` - Image processing
- `numpy` - Numerical computations
- `textblob` - Natural language processing
- `anthropic` - Claude AI integration
- `google-cloud-vision` - Google Vision API
- `google-cloud-documentai` - Document AI
### Frontend
- **Pure HTML5/CSS3/JavaScript** - No frameworks
- **Montserrat Font** - Professional typography
- **Responsive Design** - Mobile-friendly interface
---
## 📞 Support & Resources
### Getting Help
1. Check the extensive documentation in `README's/` folder
2. Review troubleshooting section in ENTERPRISE_README.md
3. Test with sample PDFs in `Test_files/`
4. Verify API keys are properly configured
### External Resources
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
- [Anthropic Claude API Docs](https://docs.anthropic.com/)
- [Google Cloud Vision Docs](https://cloud.google.com/vision/docs)
- [PDF/UA Standard](https://www.pdfa.org/resource/pdfua-in-a-nutshell/)
---
## 🌟 What Makes This Special
**Quality-First Design** - Uses best-in-class AI models (Claude, Google)
**Production-Ready** - Enterprise-grade code and architecture
**Complete Package** - Nothing else to buy or build
**Well-Documented** - 140KB+ of comprehensive guides
**Cost-Optimized** - Smart caching reduces API costs
**Three Interfaces** - Web, CLI, and REST API
**Easy Integration** - Simple REST API for existing systems
**Proven Technology** - Built on industry-standard libraries
---
## 📊 Current Status Summary
| Aspect | Status | Notes |
|--------|--------|-------|
| **Core Functionality** | ✅ Complete | All checks implemented |
| **Web Interface** | ✅ Complete | Drag-drop, progress, results |
| **REST API** | ✅ Complete | All endpoints functional |
| **CLI** | ✅ Complete | Full command-line support |
| **AI Integration** | ✅ Complete | Claude + Google Vision |
| **Auto-Remediation** | ✅ Complete | Fixes metadata issues |
| **Visual Inspector** | ✅ Complete | Page-level issue visualization |
| **Documentation** | ✅ Extensive | 19 guides + requirements specs |
| **Testing** | ✅ Implemented | 31 automated tests, 34% coverage |
| **Authentication** | ✅ Implemented | API key-based, localhost dev mode |
| **Logging** | ✅ Implemented | Structured logs with rotation |
| **Error Handling** | ✅ Implemented | Retry logic with exponential backoff |
| **veraPDF** | ✅ Integrated | Enhanced PDF/UA validation |
| **Multi-tenancy** | ⚠️ Partial | Single deployment, multi-file |
| **Report History** | ❌ Not Implemented | No tracking over time |
---
## 🚀 Quick Start Checklist
### First-Time Setup
- [ ] Install Python 3.8+ and PHP 8.0+
- [ ] Install Tesseract, Poppler, and veraPDF: `brew install tesseract poppler php verapdf`
- [ ] Create virtual environment: `python3 -m venv venv`
- [ ] Activate venv: `source venv/bin/activate`
- [ ] Install dependencies: `pip install -r requirements.txt`
- [ ] Copy `.env.example` to `.env`
- [ ] Add Anthropic API key to `.env`
- [ ] (Optional) Add Google Cloud credentials for enhanced analysis
### Every Session
- [ ] Activate venv: `source venv/bin/activate`
- [ ] Start server: `php -S localhost:8000`
- [ ] Open browser: `http://localhost:8000`
- [ ] Upload PDF and review accessibility report
### Testing & Validation
- [ ] Run tests: `pytest tests/ -v`
- [ ] Check logs: `tail -f logs/pdf_checker.log`
- [ ] Generate API key: `curl 'http://localhost:8000/auth.php?generate'`
- [ ] Test veraPDF: `verapdf --defaultflavour ua1 Test_files/sample_good.pdf`
**Estimated setup time: 15 minutes (first time), 30 seconds (subsequent sessions)**
---
**Built with ❤️ for web accessibility. Making the internet accessible for everyone.**
See `docker-compose.prod.yml` for production setup with Caddy auto-SSL.

1528
api.php

File diff suppressed because it is too large Load diff

198
auth.php
View file

@ -1,198 +0,0 @@
<?php
/**
* API Authentication Module
*
* Provides simple API key authentication for REST API endpoints
* Supports multiple authentication methods:
* - Authorization: Bearer <token>
* - X-API-Key: <key>
* - Query parameter: ?api_key=<key> (dev only)
*/
/**
* Check if request is authenticated
*
* @return bool True if authenticated, false otherwise
*/
function authenticate() {
// Development mode: allow localhost without auth
if (isDevelopmentMode()) {
return true;
}
$api_key = extractApiKey();
if (!$api_key) {
return false;
}
// Validate against configured keys
$valid_keys = getValidApiKeys();
return in_array($api_key, $valid_keys, true);
}
/**
* Check if running in development mode (localhost)
*
* @return bool True if development mode
*/
function isDevelopmentMode() {
// DEV_MODE env var explicitly bypasses auth (set in Apache/env config)
$dev_mode = getenv('DEV_MODE');
return ($dev_mode === 'true' || $dev_mode === '1');
}
/**
* Extract API key from request
*
* Checks multiple sources in order of security:
* 1. Authorization: Bearer header
* 2. X-API-Key header
* 3. Query parameter (least secure, for dev only)
*
* @return string|null API key or null if not found
*/
function extractApiKey() {
// Check Authorization: Bearer header
if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
if (preg_match('/Bearer\s+(.*)$/i', $_SERVER['HTTP_AUTHORIZATION'], $matches)) {
return trim($matches[1]);
}
}
// Check X-API-Key header
if (isset($_SERVER['HTTP_X_API_KEY'])) {
return trim($_SERVER['HTTP_X_API_KEY']);
}
// Check query parameter (least secure - dev only)
if (isDevelopmentMode() && isset($_GET['api_key'])) {
return trim($_GET['api_key']);
}
return null;
}
/**
* Get list of valid API keys
*
* Loads keys from:
* 1. Environment variable API_KEY
* 2. .api_keys file (one key per line)
* 3. Default dev key (for development only)
*
* @return array List of valid API keys
*/
function getValidApiKeys() {
$keys = [];
// Load from environment variable
$env_key = getenv('API_KEY');
if ($env_key) {
$keys[] = $env_key;
}
// Load from .api_keys file
$config_file = __DIR__ . '/.api_keys';
if (file_exists($config_file)) {
$file_keys = file($config_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($file_keys) {
// Filter out comments and empty lines
$file_keys = array_filter($file_keys, function($line) {
$line = trim($line);
return $line && substr($line, 0, 1) !== '#';
});
$keys = array_merge($keys, array_values($file_keys));
}
}
// Fallback to dev key only in development mode
if (empty($keys) && isDevelopmentMode()) {
error_log("WARNING: Using default dev API key. Configure proper API keys for production!");
$keys[] = 'dev_key_12345';
}
return array_unique($keys);
}
/**
* Send error response and exit
*
* @param string $message Error message
* @param int $status_code HTTP status code
*/
function sendUnauthorizedResponse($message = "Unauthorized", $status_code = 401) {
http_response_code($status_code);
header('Content-Type: application/json');
header('WWW-Authenticate: Bearer realm="API"');
echo json_encode([
'success' => false,
'error' => $message,
'status' => $status_code
]);
exit;
}
/**
* Require authentication or send error
*
* Call this at the beginning of protected endpoints
*/
function requireAuth() {
if (!authenticate()) {
sendUnauthorizedResponse("Valid API key required");
}
}
/**
* Generate a new random API key
*
* @return string 64-character hex API key
*/
function generateApiKey() {
return bin2hex(random_bytes(32));
}
// Example usage (for testing):
if (basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) {
header('Content-Type: text/plain');
echo "PDF Accessibility Checker - Authentication Module\n";
echo "=================================================\n\n";
if (isset($_GET['generate'])) {
echo "New API Key:\n";
echo generateApiKey() . "\n\n";
echo "Add this to your .api_keys file or API_KEY environment variable.\n";
} else if (isset($_GET['test'])) {
echo "Testing authentication...\n\n";
$api_key = extractApiKey();
if ($api_key) {
echo "API Key found: " . substr($api_key, 0, 8) . "...\n";
if (authenticate()) {
echo "✅ Authentication successful!\n";
} else {
echo "❌ Authentication failed - invalid key\n";
}
} else {
echo "❌ No API key provided\n";
echo "\nTry:\n";
echo " - Add header: X-API-Key: <your-key>\n";
echo " - Or query param: ?api_key=<your-key>&test=1\n";
}
echo "\nValid keys configured: " . count(getValidApiKeys()) . "\n";
} else {
echo "Available actions:\n";
echo " ?generate - Generate new API key\n";
echo " ?test - Test authentication\n";
echo "\nExample:\n";
echo " php auth.php?generate\n";
echo " curl -H 'X-API-Key: your-key' http://localhost:8000/auth.php?test\n";
}
}
?>

View file

@ -1,14 +0,0 @@
steps:
- name: 'gcr.io/cloud-builders/docker'
args:
- 'build'
- '-t'
- 'us-central1-docker.pkg.dev/optical-414516/pdf-accessibility/checker:latest'
- '-f'
- 'Dockerfile.cloudrun'
- '.'
images:
- 'us-central1-docker.pkg.dev/optical-414516/pdf-accessibility/checker:latest'
timeout: '600s'

View file

@ -26,7 +26,7 @@ logger = logging.getLogger('cloudrun')
app = Flask(__name__)
GCS_BUCKET_NAME = os.getenv('GCS_BUCKET_NAME', 'optical-pdf-images')
GCS_BUCKET_NAME = os.getenv('STORAGE_BUCKET', 'pdf-pages')
def upload_images_to_gcs(images_dir: Path, job_id: str) -> dict:

View file

@ -1,7 +1,7 @@
/* Enterprise PDF Accessibility Checker — Redesigned */
/* Aesthetic: Precision Observatory — utilitarian elegance with warm accents */
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
*, *::before, *::after {
margin: 0;
@ -9,18 +9,18 @@
box-sizing: border-box;
}
/* ── Design Tokens — Oliver Branding ── */
/* ── Design Tokens — Aimpress ── */
:root {
/* Typography */
--font-display: 'Montserrat', sans-serif;
--font-body: 'Montserrat', sans-serif;
--font-display: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
/* Core palette — Oliver yellow + black */
--accent: #FFC407;
--accent-hover: #e6b006;
--accent-glow: rgba(255, 196, 7, 0.2);
--accent-subtle: rgba(255, 196, 7, 0.08);
--accent-text: #000000; /* text on accent backgrounds */
/* Core palette — Aimpress indigo */
--accent: #6366F1;
--accent-hover: #4F46E5;
--accent-glow: rgba(99, 102, 241, 0.2);
--accent-subtle: rgba(99, 102, 241, 0.08);
--accent-text: #ffffff; /* text on accent backgrounds */
/* Semantic */
--success: #059669;
@ -48,7 +48,7 @@
--border-subtle: #eae8e4;
--divider: #d4d0ca;
--log-bg: #faf9f7;
--primary: #FFC407;
--primary: #6366F1;
--primary-dark: #e6b006;
--black: #1a1a1a;
@ -84,10 +84,10 @@
--border-subtle: #2a2a2a;
--divider: #303030;
--log-bg: #121212;
--primary: #FFC407;
--primary: #6366F1;
--primary-dark: #ffd54f;
--black: #f0f0f0;
--accent: #FFC407;
--accent: #6366F1;
--accent-hover: #ffd54f;
--accent-glow: rgba(255, 196, 7, 0.25);
--accent-subtle: rgba(255, 196, 7, 0.1);

226
deploy.sh
View file

@ -1,226 +0,0 @@
#!/usr/bin/env bash
#
# deploy.sh — Idempotent deployment script for PDF Accessibility Checker
#
# Usage:
# cd /opt/pdf-accessibility && ./deploy.sh
#
# Architecture:
# - Apache (host) serves frontend + api.php from /var/www/html/pdf-accessibility
# - Docker Compose runs: PostgreSQL
# - PDF processing via Google Cloud Run (synchronous HTTP call from api.php)
#
set -euo pipefail
# ── Configuration ─────────────────────────────────────────────────
REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
WEB_DIR="/var/www/html/pdf-accessibility"
COMPOSE_FILE="docker-compose.prod.yml"
ENV_FILE="${REPO_DIR}/.env"
MIN_PHP_VERSION="8.0"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[DEPLOY]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
err() { echo -e "${RED}[ERROR]${NC} $*"; }
# ── Preflight Checks ─────────────────────────────────────────────
log "Starting deployment from ${REPO_DIR}"
# Check Docker
if ! command -v docker &>/dev/null; then
err "Docker is not installed. Install it first:"
err " curl -fsSL https://get.docker.com | sh"
err " sudo usermod -aG docker \$USER"
exit 1
fi
# Check Docker Compose (v2 plugin)
if ! docker compose version &>/dev/null; then
err "Docker Compose v2 is not available. Install it:"
err " sudo apt-get install docker-compose-plugin"
exit 1
fi
# Check PHP
if ! command -v php &>/dev/null; then
warn "PHP is not installed. api.php requires PHP ${MIN_PHP_VERSION}+ with extensions:"
warn " sudo apt-get install php8.2 php8.2-pgsql php8.2-curl php8.2-mbstring"
else
PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
log "PHP version: ${PHP_VER}"
# Check required extensions
MISSING_EXT=""
php -m | grep -qi pgsql || MISSING_EXT="${MISSING_EXT} php-pgsql"
php -m | grep -qi curl || MISSING_EXT="${MISSING_EXT} php-curl"
php -m | grep -qi openssl || MISSING_EXT="${MISSING_EXT} php-openssl"
if [ -n "${MISSING_EXT}" ]; then
warn "Missing PHP extensions:${MISSING_EXT}"
warn "Install with: sudo apt-get install${MISSING_EXT}"
fi
fi
# ── Pull Latest Code ─────────────────────────────────────────────
log "Pulling latest code..."
cd "${REPO_DIR}"
if [ -d .git ]; then
git config core.fileMode false
# Run git as the repo owner (not root) so SSH keys work
REPO_OWNER=$(stat -c '%U' "${REPO_DIR}/.git")
if [ "$(id -u)" = "0" ] && [ "${REPO_OWNER}" != "root" ]; then
sudo -u "${REPO_OWNER}" git -C "${REPO_DIR}" fetch --all
sudo -u "${REPO_OWNER}" git -C "${REPO_DIR}" reset --hard origin/$(git rev-parse --abbrev-ref HEAD)
else
git fetch --all
git reset --hard origin/$(git rev-parse --abbrev-ref HEAD)
fi
log "Code updated to $(git log --oneline -1)"
else
warn "Not a git repo — using existing files"
fi
# ── Environment File ─────────────────────────────────────────────
if [ ! -f "${ENV_FILE}" ]; then
log "Creating .env from .env.example (first run)..."
cp "${REPO_DIR}/.env.example" "${ENV_FILE}"
# Override Docker hostnames with localhost for host-side PHP
sed -i 's/^DB_HOST=postgres/DB_HOST=127.0.0.1/' "${ENV_FILE}"
sed -i 's/^DEV_MODE=true/DEV_MODE=false/' "${ENV_FILE}"
warn "Review and update ${ENV_FILE} with production values:"
warn " - DB_PASSWORD (change from default!)"
warn " - ANTHROPIC_API_KEY"
warn " - GOOGLE_API_KEY"
warn " - CLOUD_RUN_URL"
warn " - GCP_SA_KEY_PATH (copy pdf-api-invoker-key.json to server)"
warn " - AZURE_* settings"
else
log "Using existing .env file"
fi
# ── Build Docker Containers ──────────────────────────────────────
log "Building Docker containers (using cache)..."
docker compose -f "${COMPOSE_FILE}" build
log "Starting/restarting Docker services..."
docker compose -f "${COMPOSE_FILE}" up -d --remove-orphans
# Wait for PostgreSQL to be ready
log "Waiting for PostgreSQL to be healthy..."
RETRIES=30
until docker compose -f "${COMPOSE_FILE}" exec -T postgres pg_isready -U pdf_checker &>/dev/null || [ $RETRIES -eq 0 ]; do
sleep 1
RETRIES=$((RETRIES - 1))
done
if [ $RETRIES -eq 0 ]; then
err "PostgreSQL failed to start. Check logs:"
err " docker compose -f ${COMPOSE_FILE} logs postgres"
exit 1
fi
log "PostgreSQL is ready"
# Database init.sql runs automatically on first compose up via
# /docker-entrypoint-initdb.d/init.sql — no migration tool needed.
# For future migrations, add numbered SQL files to db/ and apply:
if [ -d "${REPO_DIR}/db/migrations" ]; then
for migration in "${REPO_DIR}"/db/migrations/*.sql; do
[ -f "$migration" ] || continue
MIGRATION_NAME=$(basename "$migration")
log "Applying migration: ${MIGRATION_NAME}"
docker compose -f "${COMPOSE_FILE}" exec -T postgres \
psql -U pdf_checker -d pdf_checker -f "/dev/stdin" < "$migration" 2>/dev/null || \
warn "Migration ${MIGRATION_NAME} may have already been applied"
done
fi
# ── Deploy Frontend Files ─────────────────────────────────────────
log "Deploying frontend to ${WEB_DIR}..."
# Create web directory if it doesn't exist
sudo mkdir -p "${WEB_DIR}"
# Clean old frontend files (but preserve uploads, results, .env, logs)
log "Cleaning old frontend files..."
sudo rm -f "${WEB_DIR}/index.html" "${WEB_DIR}/history.html"
sudo rm -rf "${WEB_DIR}/css" "${WEB_DIR}/js"
sudo rm -f "${WEB_DIR}/api.php" "${WEB_DIR}/auth.php"
# Copy frontend files
sudo cp "${REPO_DIR}/index.html" "${WEB_DIR}/"
sudo cp "${REPO_DIR}/history.html" "${WEB_DIR}/"
sudo cp -r "${REPO_DIR}/css" "${WEB_DIR}/"
sudo cp -r "${REPO_DIR}/js" "${WEB_DIR}/"
# Copy PHP backend files
sudo cp "${REPO_DIR}/api.php" "${WEB_DIR}/"
sudo cp "${REPO_DIR}/auth.php" "${WEB_DIR}/"
# Copy Python scripts (needed if api.php fallback exec() is used)
sudo cp "${REPO_DIR}/enterprise_pdf_checker.py" "${WEB_DIR}/"
sudo cp "${REPO_DIR}/pdf_remediation.py" "${WEB_DIR}/"
sudo cp "${REPO_DIR}/logger_config.py" "${WEB_DIR}/"
sudo cp "${REPO_DIR}/retry_helper.py" "${WEB_DIR}/"
# Copy .env for PHP (if not already there)
if [ ! -f "${WEB_DIR}/.env" ]; then
sudo cp "${ENV_FILE}" "${WEB_DIR}/.env"
log "Copied .env to web directory"
else
# Update .env in web dir from repo .env
sudo cp "${ENV_FILE}" "${WEB_DIR}/.env"
fi
# Create runtime directories
sudo mkdir -p "${WEB_DIR}/uploads" "${WEB_DIR}/results" "${WEB_DIR}/logs" "${WEB_DIR}/rate_limits"
# Set ownership for Apache
sudo chown -R www-data:www-data "${WEB_DIR}"
sudo chmod -R 755 "${WEB_DIR}"
sudo chmod -R 775 "${WEB_DIR}/uploads" "${WEB_DIR}/results" "${WEB_DIR}/logs" "${WEB_DIR}/rate_limits"
# ── Verify ────────────────────────────────────────────────────────
log ""
log "============================================="
log " Deployment complete!"
log "============================================="
log ""
log "Services status:"
docker compose -f "${COMPOSE_FILE}" ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"
log ""
log "Frontend: ${WEB_DIR}"
log "Docker: PostgreSQL (127.0.0.1:1221)"
log "Cloud Run: ${CLOUD_RUN_URL:-$(grep '^CLOUD_RUN_URL=' "${ENV_FILE}" 2>/dev/null | cut -d= -f2 || echo 'not set')}"
log ""
# Quick health check
if docker compose -f "${COMPOSE_FILE}" exec -T postgres pg_isready -U pdf_checker &>/dev/null; then
log "PostgreSQL: OK"
fi
log ""
log "Reloading Apache..."
sudo systemctl reload apache2 && log "Apache reloaded" || warn "Apache reload failed — run: sudo systemctl reload apache2"
log ""
log "Next steps (if first deploy):"
log " 1. Ensure pdf-api-invoker-key.json is at the GCP_SA_KEY_PATH location"
log " 2. Review ${WEB_DIR}/.env (especially CLOUD_RUN_URL and API keys)"
log ""

View file

@ -1,15 +0,0 @@
#!/bin/sh
set -e
# Allow PHP-FPM to inherit environment variables (needed for getenv() in PHP)
# By default PHP-FPM clears the environment; this disables that behavior
echo 'clear_env = no' >> /usr/local/etc/php-fpm.d/www.conf
# 15-minute timeout for Cloud Run PDF processing
echo 'request_terminate_timeout = 900' >> /usr/local/etc/php-fpm.d/www.conf
# Start PHP-FPM in background
php-fpm -D
# Start Nginx in foreground
nginx -g 'daemon off;'

View file

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Documents — PDF Accessibility Checker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<div id="msalConfig" hidden
data-tenant-id="e519c2e6-bc6d-4fdf-8d9c-923c2f002385"
data-client-id="9079054c-9620-4757-a256-23413042f1ef"
data-redirect-uri="https://ai-sandbox.oliver.solutions/pdf-accessibility/history.html"></div>
<!-- Auth Overlay -->
<div class="auth-overlay" id="authOverlay" role="dialog" aria-label="Sign in required" aria-modal="true" aria-describedby="authCardDesc">
<div class="auth-card">
<h2>PDF Accessibility Checker</h2>
<p id="authCardDesc">Sign in with your organization account to continue.</p>
<button class="btn-microsoft" onclick="loginWithMicrosoft()" aria-label="Sign in with Microsoft">
<svg width="20" height="20" viewBox="0 0 21 21" aria-hidden="true"><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>
Sign in with Microsoft
</button>
</div>
</div>
<header>
<div class="container">
<div class="header-inner">
<div>
<h1>Enterprise PDF Accessibility Checker</h1>
<p class="subtitle">Comprehensive WCAG 2.1 compliance validation with AI-powered analysis</p>
</div>
<div class="header-actions">
<a href="index.html" class="btn btn-secondary" style="text-decoration:none;padding:8px 16px;font-size:13px;">&#x2B06; New Check</a>
<span class="user-info" id="userInfo"></span>
<button id="logoutBtn" onclick="logout()" style="display:none;">Sign Out</button>
<button id="themeToggle" onclick="toggleDarkMode()" aria-label="Toggle dark mode">Dark</button>
</div>
</div>
</div>
</header>
<main id="main-content">
<div class="container" style="padding-top: 32px;">
<div class="card" id="historySection" style="display:none;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h2 style="margin:0;">My Documents</h2>
<button class="btn btn-secondary" onclick="loadHistory()" aria-label="Refresh" style="padding:8px 16px;font-size:13px;">&#x21BA; Refresh</button>
</div>
<p style="font-size:13px;color:var(--text-muted);margin-bottom:20px;padding:8px 12px;background:var(--surface-alt);border-radius:6px;">
Documents are retained for <strong>30 days</strong> after upload. Download reports before they expire.
</p>
<div id="historyTableWrap">
<p style="color:var(--text-muted);font-size:14px;" id="historyEmpty">No documents checked yet. <a href="index.html">Upload a PDF</a> to get started.</p>
</div>
</div>
</div>
</main>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/history.js"></script>
<script src="js/app-history.js"></script>
</body>
</html>

View file

@ -1,266 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enterprise PDF Accessibility Checker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- MSAL config (values injected from env or kept as data attributes) -->
<div id="msalConfig" hidden
data-tenant-id="e519c2e6-bc6d-4fdf-8d9c-923c2f002385"
data-client-id="9079054c-9620-4757-a256-23413042f1ef"
data-redirect-uri="https://ai-sandbox.oliver.solutions/pdf-accessibility"></div>
<!-- Auth Overlay (Azure AD / MSAL) -->
<div class="auth-overlay" id="authOverlay" role="dialog" aria-label="Sign in required" aria-modal="true" aria-describedby="authCardDesc">
<div class="auth-card">
<h2>PDF Accessibility Checker</h2>
<p id="authCardDesc">Sign in with your organization account to continue.</p>
<button class="btn-microsoft" onclick="loginWithMicrosoft()" aria-label="Sign in with Microsoft">
<svg width="20" height="20" viewBox="0 0 21 21" aria-hidden="true"><rect x="1" y="1" width="9" height="9" fill="#f25022"/><rect x="11" y="1" width="9" height="9" fill="#7fba00"/><rect x="1" y="11" width="9" height="9" fill="#00a4ef"/><rect x="11" y="11" width="9" height="9" fill="#ffb900"/></svg>
Sign in with Microsoft
</button>
</div>
</div>
<header>
<div class="container">
<div class="header-inner">
<div>
<h1>Enterprise PDF Accessibility Checker</h1>
<p class="subtitle">Comprehensive WCAG 2.1 compliance validation with AI-powered analysis</p>
</div>
<div class="header-actions">
<a href="history.html" id="historyLink" style="display:none;text-decoration:none;" class="btn btn-secondary" style="padding:8px 16px;font-size:13px;">&#x1F4C2; My Documents</a>
<span class="user-info" id="userInfo"></span>
<button id="logoutBtn" onclick="logout()" style="display:none;">Sign Out</button>
<button id="themeToggle" onclick="toggleDarkMode()" aria-label="Toggle dark mode">Dark</button>
</div>
</div>
</div>
</header>
<main id="main-content">
<div class="container">
<!-- Upload Section -->
<div class="card" id="uploadSection">
<h2>Upload PDF Document</h2>
<div class="upload-mode-tabs" role="tablist" aria-label="Upload mode">
<button class="upload-tab active" id="tabSingle" role="tab" aria-selected="true" aria-controls="singleUploadArea" onclick="switchUploadMode('single')">Single File</button>
<button class="upload-tab" id="tabBatch" role="tab" aria-selected="false" aria-controls="batchUploadArea" onclick="switchUploadMode('batch')">Batch Upload</button>
</div>
<div id="singleUploadArea" role="tabpanel" aria-labelledby="tabSingle" tabindex="0">
<div class="upload-area" id="uploadArea" role="button" tabindex="0" aria-label="Drop PDF here or click to browse">
<div class="upload-icon">&#x1F4C4;</div>
<div class="upload-text">Drop your PDF here or click to browse</div>
<div class="upload-hint">Maximum file size: 50MB</div>
<input type="file" id="fileInput" accept=".pdf" aria-hidden="true">
</div>
<div class="upload-ready" id="uploadReadyState" aria-live="polite">
<div class="ready-filename" id="readyFilename"></div>
<div class="ready-filesize" id="readyFilesize"></div>
<button class="btn-start" onclick="beginCheck()" aria-label="Start accessibility check">
&#x25B6; Start Accessibility Check
</button>
<button class="btn-remove" onclick="removeFile()" aria-label="Remove file">Remove</button>
</div>
</div>
<div id="batchUploadArea" style="display:none;" role="tabpanel" aria-labelledby="tabBatch" tabindex="-1">
<div class="upload-area" id="batchDropArea" role="button" tabindex="0" aria-label="Drop multiple PDFs here or click to browse">
<div class="upload-icon">&#x1F4DA;</div>
<div class="upload-text">Drop multiple PDFs here or click to browse</div>
<div class="upload-hint">Maximum 10 files, 50MB each</div>
<input type="file" id="batchFileInput" accept=".pdf" multiple aria-hidden="true">
</div>
<div id="batchFileList" style="display:none;margin-top:15px;"></div>
<div id="batchActions" style="display:none;margin-top:15px;gap:10px;">
<button class="btn btn-primary" onclick="startBatchUpload()" id="batchUploadBtn">Upload &amp; Check All</button>
<button class="btn btn-secondary" onclick="clearBatchFiles()">Clear</button>
</div>
<div id="batchProgress" style="display:none;margin-top:20px;"></div>
</div>
<div class="api-config">
<h3 style="margin-bottom:15px;">Check Options</h3>
<div class="form-group" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;">
<input type="checkbox" id="quickMode" style="width:auto;height:18px;cursor:pointer;">
<label for="quickMode" style="cursor:pointer;margin:0;font-weight:600;">
Quick Mode (Skip AI analysis, OCR, and color contrast)
</label>
</div>
<div class="help-text">
Quick mode runs basic checks only — great for initial scans. Completes in ~10 seconds vs ~2 minutes.
</div>
</div>
<div class="progress-container" id="progressContainer" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Analysis progress">
<div class="progress-header">
<div class="progress-text" id="progressText">Uploading...</div>
<div class="progress-percent" id="progressPercent">0%</div>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width:0%"></div>
</div>
<div class="progress-log" id="progressLog">
<div class="log-header">Processing Details</div>
<div class="log-content" id="logContent" aria-live="polite">
<div class="log-entry" role="status">Initializing...</div>
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div class="results" id="resultsSection">
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
<h2>Accessibility Report</h2>
<div style="display:flex;gap:10px;">
<button class="btn btn-secondary" onclick="exportReport('html')" id="exportHtmlBtn" title="Download HTML report">Export Report</button>
<button class="btn btn-secondary" onclick="exportReport('json')" id="exportJsonBtn" title="Download JSON data">Export JSON</button>
<button class="btn btn-secondary" onclick="exportReport('pdf')" id="exportPdfBtn" title="Download PDF report (PAC-style)">&#x1F4C4; PDF Report</button>
<button class="btn btn-secondary" onclick="resetCheck()">Check Another PDF</button>
</div>
</div>
<div class="score-display">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;">
<output class="score-number" id="scoreNumber" aria-label="Accessibility score">--</output>
<span class="score-adjusted-label" id="adjustedLabel" style="display:none;">(Adjusted)</span>
</div>
<div>
<div class="score-label">Accessibility Score</div>
<button id="recheckBtn" class="btn-recheck" onclick="recalculateScore()"
style="display:none;"
title="Recalculate score applying dismissed issues and manual overrides">
Recalculate Score
</button>
</div>
</div>
<div class="stats-grid" id="statsGrid" role="group" aria-label="Issue severity counts"></div>
<div id="wcagCompliance" style="display:none;" aria-label="WCAG conformance level status"></div>
<div id="scoreBreakdown"></div>
</div>
<!-- Auto-Fix Card -->
<div class="card" id="remediationCard" style="display:none;">
<h2>Auto-Fix Available</h2>
<p style="color:var(--text-light);margin-bottom:15px;">
<span id="fixableCount">0</span> issues can be automatically fixed.
</p>
<div id="fixesList" style="margin-bottom:15px;"></div>
<button class="btn btn-primary" onclick="applyFixes()" id="applyFixesBtn" style="display:inline-flex;align-items:center;gap:8px;">
<span>Apply Automatic Fixes</span>
</button>
<div id="fixResult" style="margin-top:15px;display:none;" role="alert"></div>
</div>
<!-- Next Steps Card -->
<div class="card" id="nextStepsCard" style="display:none;">
<h2>Recommended Next Steps</h2>
<p style="color:var(--text-muted);font-size:13px;margin-bottom:16px;">Prioritised actions to improve accessibility — fix in this order for maximum impact.</p>
<ol id="nextStepsList" style="list-style:none;padding:0;margin:0;"></ol>
</div>
<!-- Matterhorn Protocol Card -->
<div class="card" id="matterhornCard" style="display:none;">
<h2>Matterhorn Protocol — PDF/UA-1</h2>
<div id="matterhornBanner"></div>
<table id="matterhornTable" aria-label="Matterhorn Protocol checkpoints">
<thead>
<tr>
<th>Checkpoint</th>
<th>How</th>
<th>Status</th>
</tr>
</thead>
<tbody id="matterhornBody"></tbody>
</table>
</div>
<!-- Visual Page Viewer -->
<div class="card" id="pageViewerCard" style="display:none;">
<h2>Visual Page Inspector</h2>
<p style="color:var(--text-light);margin-bottom:20px;">Click on issues to see their exact location on the page</p>
<div class="page-viewer-layout" style="display:flex;gap:20px;align-items:flex-start;">
<div class="page-selector-wrap" style="flex-shrink:0;">
<div style="background:var(--surface);padding:15px;border-radius:8px;min-width:150px;">
<h3 style="font-size:14px;margin-bottom:10px;">Select Page</h3>
<div id="pageSelector" style="display:flex;flex-direction:column;gap:5px;" role="tablist"></div>
</div>
</div>
<div style="flex:1;background:var(--surface-alt);border-radius:8px;padding:20px;position:relative;min-height:600px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
<h3 id="currentPageTitle" style="font-size:16px;margin:0;">Page 1</h3>
<div style="display:flex;gap:10px;">
<button onclick="zoomOut()" style="padding:8px 12px;border:1px solid var(--border);background:var(--surface);border-radius:6px;cursor:pointer;color:var(--text);" aria-label="Zoom out">-</button>
<span id="zoomLevel" style="padding:8px 12px;background:var(--surface);border-radius:6px;min-width:60px;text-align:center;">100%</span>
<button onclick="zoomIn()" style="padding:8px 12px;border:1px solid var(--border);background:var(--surface);border-radius:6px;cursor:pointer;color:var(--text);" aria-label="Zoom in">+</button>
<button onclick="resetZoom()" style="padding:8px 12px;border:1px solid var(--border);background:var(--surface);border-radius:6px;cursor:pointer;color:var(--text);" aria-label="Reset zoom to 100%">Reset</button>
</div>
</div>
<div id="pageImageContainer" style="overflow:auto;max-height:800px;background:white;border-radius:8px;position:relative;">
<div id="zoomContainer" style="position:relative;display:inline-block;transform-origin:top left;">
<img id="pageImage" src="" alt="PDF Page" style="display:block;max-width:100%;">
<svg id="markerOverlay" style="position:absolute;top:0;left:0;pointer-events:none;width:100%;height:100%;"></svg>
</div>
</div>
<div id="markerLegend" style="margin-top:15px;padding:15px;background:var(--surface);border-radius:8px;" role="region" aria-label="Issue location legend">
<strong>Legend:</strong>
<span style="margin-left:10px;padding:4px 8px;background:#dc2626;color:white;border-radius:4px;font-size:12px;">Critical</span>
<span style="margin-left:10px;padding:4px 8px;background:#ef4444;color:white;border-radius:4px;font-size:12px;">Error</span>
<span style="margin-left:10px;padding:4px 8px;background:#f59e0b;color:white;border-radius:4px;font-size:12px;">Warning</span>
<span style="margin-left:10px;padding:4px 8px;background:#3b82f6;color:white;border-radius:4px;font-size:12px;">Info</span>
</div>
</div>
</div>
</div>
<div class="card">
<h2>Issues &amp; Recommendations</h2>
<div class="filters" role="toolbar" aria-label="Filter issues by severity">
<button class="filter-btn active" onclick="filterIssues('all')" aria-pressed="true">All</button>
<button class="filter-btn" onclick="filterIssues('CRITICAL')" aria-pressed="false">Critical</button>
<button class="filter-btn" onclick="filterIssues('ERROR')" aria-pressed="false">Errors</button>
<button class="filter-btn" onclick="filterIssues('WARNING')" aria-pressed="false">Warnings</button>
<button class="filter-btn" onclick="filterIssues('INFO')" aria-pressed="false">Info</button>
</div>
<div id="issuesList" role="list"></div>
<div style="margin-top:28px;padding-top:20px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;">
<span style="font-size:13px;color:var(--text-muted);">Review complete — check another document or export your report.</span>
<button class="btn btn-primary" onclick="resetCheck()" style="padding:12px 28px;font-size:15px;">&#x2B06; Check Another PDF</button>
</div>
</div>
</div>
</div>
</main>
<!-- JS Modules -->
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
<script src="js/upload.js"></script>
<script src="js/batch.js"></script>
<script src="js/results.js"></script>
<script src="js/page-viewer.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View file

@ -1,86 +0,0 @@
/* API communication layer */
const API_BASE = 'api.php';
async function apiCall(action, options = {}) {
const { method = 'GET', body = null, params = {} } = options;
let url = API_BASE;
const queryParams = new URLSearchParams({ action, ...params });
if (method === 'GET') {
url += '?' + queryParams.toString();
}
const headers = {};
// Add MSAL token if available
if (window.msalToken) {
headers['Authorization'] = 'Bearer ' + window.msalToken;
}
const fetchOptions = { method, headers };
if (body) {
if (body instanceof FormData) {
body.append('action', action);
fetchOptions.body = body;
} else {
fetchOptions.body = body;
}
}
const response = await fetch(url, fetchOptions);
return response.json();
}
async function uploadFile(file) {
const formData = new FormData();
formData.append('pdf', file);
return apiCall('upload', { method: 'POST', body: formData });
}
async function startCheck(jobId, quickMode) {
const formData = new FormData();
formData.append('job_id', jobId);
if (quickMode) formData.append('quick_mode', '1');
return apiCall('check', { method: 'POST', body: formData });
}
async function checkStatus(jobId) {
return apiCall('status', { params: { job_id: jobId } });
}
async function getResult(jobId) {
return apiCall('result', { params: { job_id: jobId } });
}
async function getDebugInfo(jobId) {
return apiCall('debug', { params: { job_id: jobId } });
}
async function remediatePdf(jobId) {
const formData = new FormData();
formData.append('job_id', jobId);
return apiCall('remediate', { method: 'POST', body: formData });
}
async function getStats() {
return apiCall('stats');
}
async function uploadBatch(files) {
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('pdfs[]', files[i]);
}
return apiCall('batch_upload', { method: 'POST', body: formData });
}
async function checkBatchStatus(batchId) {
return apiCall('batch_status', { params: { batch_id: batchId } });
}
function getExportUrl(jobId, format) {
const params = new URLSearchParams({ action: 'export', job_id: jobId, format: format });
return API_BASE + '?' + params.toString();
}

View file

@ -1,96 +0,0 @@
/* MSAL auth + init for history.html */
const msalConfig = {
auth: {
clientId: '',
authority: '',
redirectUri: window.location.origin + window.location.pathname
},
cache: { cacheLocation: 'localStorage', storeAuthStateInCookie: false }
};
let msalInstance = null;
window.msalToken = null;
function initMsal() {
const el = document.getElementById('msalConfig');
if (!el) return;
const tenantId = el.dataset.tenantId;
const clientId = el.dataset.clientId;
const redirectUri = el.dataset.redirectUri;
if (!tenantId || !clientId) return;
msalConfig.auth.clientId = clientId;
msalConfig.auth.authority = `https://login.microsoftonline.com/${tenantId}`;
if (redirectUri) msalConfig.auth.redirectUri = redirectUri;
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@azure/msal-browser@2/lib/msal-browser.min.js';
script.onload = () => {
msalInstance = new msal.PublicClientApplication(msalConfig);
msalInstance.initialize().then(handleMsalRedirect);
};
document.head.appendChild(script);
}
async function handleMsalRedirect() {
try {
const response = await msalInstance.handleRedirectPromise();
if (response) {
window.msalToken = response.accessToken;
showAuthenticatedUI(response.account);
return;
}
} catch (e) { console.error('MSAL redirect error:', e); }
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
try {
const tokenResponse = await msalInstance.acquireTokenSilent({ scopes: ['User.Read'], account: accounts[0] });
window.msalToken = tokenResponse.accessToken;
showAuthenticatedUI(accounts[0]);
} catch (e) { showLoginUI(); }
} else {
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
showAuthenticatedUI(null);
} else {
showLoginUI();
}
}
}
function showLoginUI() {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.add('active');
}
function showAuthenticatedUI(account) {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.remove('active');
const userInfo = document.getElementById('userInfo');
if (userInfo && account) userInfo.textContent = account.name || account.username;
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) logoutBtn.style.display = 'inline-block';
const historySection = document.getElementById('historySection');
if (historySection) historySection.style.display = '';
loadHistory();
}
async function loginWithMicrosoft() {
if (!msalInstance) return;
try { await msalInstance.loginRedirect({ scopes: ['User.Read'] }); }
catch (e) { console.error('Login failed:', e); alert('Login failed. Please try again.'); }
}
function logout() {
if (msalInstance) msalInstance.logoutRedirect();
}
document.addEventListener('DOMContentLoaded', () => {
loadTheme(); // from utils.js — sets data-theme on :root
initMsal();
});

154
js/app.js
View file

@ -1,154 +0,0 @@
/* App initialization and MSAL authentication */
// MSAL configuration
const msalConfig = {
auth: {
clientId: '', // Set from data attribute or env
authority: '',
redirectUri: window.location.origin + window.location.pathname
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false
}
};
let msalInstance = null;
window.msalToken = null;
function initMsal() {
const el = document.getElementById('msalConfig');
if (!el) return;
const tenantId = el.dataset.tenantId;
const clientId = el.dataset.clientId;
const redirectUri = el.dataset.redirectUri;
if (!tenantId || !clientId) return;
msalConfig.auth.clientId = clientId;
msalConfig.auth.authority = `https://login.microsoftonline.com/${tenantId}`;
if (redirectUri) msalConfig.auth.redirectUri = redirectUri;
// Load MSAL library dynamically
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/@azure/msal-browser@2/lib/msal-browser.min.js';
script.onload = () => {
msalInstance = new msal.PublicClientApplication(msalConfig);
msalInstance.initialize().then(() => {
handleMsalRedirect();
});
};
document.head.appendChild(script);
}
async function handleMsalRedirect() {
try {
const response = await msalInstance.handleRedirectPromise();
if (response) {
window.msalToken = response.accessToken;
showAuthenticatedUI(response.account);
return;
}
} catch (e) {
console.error('MSAL redirect error:', e);
}
// Check for existing session
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
try {
const tokenResponse = await msalInstance.acquireTokenSilent({
scopes: ['User.Read'],
account: accounts[0]
});
window.msalToken = tokenResponse.accessToken;
showAuthenticatedUI(accounts[0]);
} catch (e) {
// Token expired, show login
showLoginUI();
}
} else {
// Check if we're in dev mode (localhost) — skip MSAL
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
hideAuthOverlay();
} else {
showLoginUI();
}
}
}
function showLoginUI() {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.add('active');
}
function hideAuthOverlay() {
const overlay = document.getElementById('authOverlay');
if (overlay) overlay.classList.remove('active');
}
function showAuthenticatedUI(account) {
hideAuthOverlay();
const userInfo = document.getElementById('userInfo');
if (userInfo && account) {
userInfo.textContent = account.name || account.username;
}
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) logoutBtn.style.display = 'inline-block';
// Show My Documents link in header
const historyLink = document.getElementById('historyLink');
if (historyLink) historyLink.style.display = 'inline-block';
// If URL has ?job_id= open that report directly
const params = new URLSearchParams(window.location.search);
const jobId = params.get('job_id');
if (jobId) openHistoryJob(jobId);
}
async function openHistoryJob(jobId) {
currentJobId = jobId;
const uploadSection = document.getElementById('uploadSection');
const resultsSection = document.getElementById('resultsSection');
if (uploadSection) uploadSection.style.display = 'none';
if (resultsSection) resultsSection.style.display = '';
try {
const resp = await getResult(jobId);
const result = resp?.data || resp;
if (!result || result.error) {
alert('Could not load report: ' + (result?.error || 'Unknown error'));
return;
}
displayResults(result);
if (resultsSection) resultsSection.scrollIntoView({ behavior: 'smooth' });
} catch (e) {
console.error('openHistoryJob failed:', e);
alert('Failed to load report.');
}
}
async function loginWithMicrosoft() {
if (!msalInstance) return;
try {
await msalInstance.loginRedirect({ scopes: ['User.Read'] });
} catch (e) {
console.error('Login failed:', e);
alert('Login failed. Please try again.');
}
}
function logout() {
if (msalInstance) {
msalInstance.logoutRedirect();
}
}
/* App init */
document.addEventListener('DOMContentLoaded', () => {
loadTheme();
initUpload();
initBatchUpload();
initMsal();
});

View file

@ -1,42 +0,0 @@
server {
listen 80;
server_name _;
root /app;
index index.html;
client_max_body_size 55M;
# Serve static files directly
location / {
try_files $uri $uri/ /index.html;
}
# PHP processing
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# 15-minute timeout for Cloud Run PDF processing
fastcgi_read_timeout 900s;
fastcgi_send_timeout 900s;
}
# Serve page images from results
location /results/ {
alias /app/results/;
expires 1d;
add_header Cache-Control "public, immutable";
}
# Security headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
# Deny access to hidden files
location ~ /\. {
deny all;
}
}

View file

@ -195,10 +195,10 @@ def generate_html(data: dict) -> str:
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family:'Montserrat',sans-serif; background:#f8fafc; color:#1e293b; line-height:1.6; }}
.skip-link {{ position:absolute; top:-100%; left:16px; background:#FFC407; color:#000; font-size:14px; font-weight:700; padding:10px 20px; border-radius:4px; text-decoration:none; z-index:9999; }}
.skip-link {{ position:absolute; top:-100%; left:16px; background:#6366F1; color:#000; font-size:14px; font-weight:700; padding:10px 20px; border-radius:4px; text-decoration:none; z-index:9999; }}
.skip-link:focus {{ top:10px; }}
.container {{ max-width:1100px; margin:0 auto; padding:20px; }}
header {{ background:#1a1a1a; color:#fff; padding:30px 0; border-left:4px solid #FFC407; }}
header {{ background:#1a1a1a; color:#fff; padding:30px 0; border-left:4px solid #6366F1; }}
header h1 {{ font-size:24px; margin-bottom:5px; }}
header p {{ opacity:0.85; font-size:14px; }}
.card {{ background:#fff; border-radius:12px; box-shadow:0 1px 3px rgba(0,0,0,0.1); padding:25px; margin-bottom:20px; }}
@ -459,11 +459,11 @@ def generate_pdf(data: dict) -> bytes:
body {{ font-family: 'Montserrat', sans-serif; font-size: 10pt; color: #1a1a1a; line-height: 1.5; }}
.header {{ background: #1a1a1a; color: white; padding: 20px 24px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }}
.header h1 {{ font-size: 16pt; font-weight: 800; letter-spacing: -0.02em; }}
.header .accent {{ color: #FFC407; }}
.header .accent {{ color: #6366F1; }}
.header .meta {{ font-size: 9pt; opacity: 0.7; margin-top: 4px; }}
.score-block {{ display: flex; align-items: center; gap: 20px; background: #1a1a1a; color: white; padding: 16px 24px; margin-bottom: 20px; border-left: 4px solid #FFC407; }}
.score-block {{ display: flex; align-items: center; gap: 20px; background: #1a1a1a; color: white; padding: 16px 24px; margin-bottom: 20px; border-left: 4px solid #6366F1; }}
.score-num {{ font-size: 48pt; font-weight: 800; color: {score_color}; letter-spacing: -0.04em; line-height: 1; }}
.score-info h2 {{ font-size: 13pt; font-weight: 700; color: #FFC407; }}
.score-info h2 {{ font-size: 13pt; font-weight: 700; color: #6366F1; }}
.score-info p {{ font-size: 9pt; color: #ccc; margin-top: 2px; }}
.stats {{ display: flex; gap: 12px; margin-bottom: 20px; }}
.stat {{ flex: 1; padding: 12px; border-radius: 6px; text-align: center; }}
@ -478,7 +478,7 @@ def generate_pdf(data: dict) -> bytes:
.stat.info .num {{ color: #3b82f6; }}
.section {{ margin-bottom: 24px; }}
.section h2 + table {{ page-break-before: avoid; }}
.section h2 {{ font-size: 13pt; font-weight: 700; border-bottom: 2px solid #FFC407; padding-bottom: 6px; margin-bottom: 12px; }}
.section h2 {{ font-size: 13pt; font-weight: 700; border-bottom: 2px solid #6366F1; padding-bottom: 6px; margin-bottom: 12px; }}
table {{ width: 100%; border-collapse: collapse; font-size: 9pt; }}
th {{ background: #f5f4f1; padding: 6px 10px; text-align: left; font-weight: 700; font-size: 8pt; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 2px solid #ddd; }}
td {{ padding: 6px 10px; border-bottom: 1px solid #eee; vertical-align: top; }}
@ -505,7 +505,7 @@ def generate_pdf(data: dict) -> bytes:
</div>
<div style="text-align:right;font-size:9pt;color:#ccc;">
WCAG 2.1 &nbsp;·&nbsp; PDF/UA-1<br>
<span style="color:#FFC407;font-weight:700;">Oliver Solutions</span>
<span style="color:#6366F1;font-weight:700;">Aimpress</span>
</div>
</header>
@ -513,7 +513,7 @@ def generate_pdf(data: dict) -> bytes:
<div class="score-block" role="img" aria-label="Accessibility score: {score} out of 100, Grade {grade}{' (Adjusted)' if is_adjusted else ''}">
<div class="score-num" aria-hidden="true">{score}</div>
<div class="score-info">
<h2>Accessibility Score Grade {grade}{' <span style="font-size:10pt;color:#FFC407;">(Adjusted)</span>' if is_adjusted else ''}</h2>
<h2>Accessibility Score Grade {grade}{' <span style="font-size:10pt;color:#6366F1;">(Adjusted)</span>' if is_adjusted else ''}</h2>
<p>{sc.get('critical',0)} critical &nbsp; {sc.get('error',0)} errors &nbsp; {sc.get('warning',0)} warnings &nbsp; {sc.get('info',0)} info</p>
{f'<p>{breakdown.get("checks_passed",0)} of {breakdown.get("checks_total",0)} checks passed</p>' if breakdown else ''}
</div>
@ -530,7 +530,7 @@ def generate_pdf(data: dict) -> bytes:
</main>
<footer class="footer">
PDF Accessibility Checker &nbsp;·&nbsp; Enterprise Edition &nbsp;·&nbsp; Oliver Solutions &nbsp;·&nbsp; {now}
PDF Accessibility SaaS &nbsp;·&nbsp; Aimpress &nbsp;·&nbsp; {now}
</footer>
</body>
</html>"""

View file

@ -1,360 +0,0 @@
# Screen Reader Simulator - Feasibility Analysis
## What We COULD Build (Realistic)
### 1. PDF Reading Order Simulator ✅ FEASIBLE
**What it does:**
- Parse PDF structure tree
- Extract content in screen reader order
- Show exactly what would be announced
- Highlight reading order issues
**Output Example:**
```
Screen Reader Output Simulation:
-----------------------------------
[Heading Level 1] "Annual Report 2024"
[Paragraph] "This document presents..."
[Image] "Bar chart showing revenue growth" (alt text)
[Heading Level 2] "Financial Summary"
[Table with 3 columns, 5 rows]
[Header Row] "Quarter | Revenue | Profit"
[Row 1] "Q1 | $1M | $100K"
...
```
**Technical approach:**
```python
def simulate_screen_reader_output(pdf_path):
# Parse structure tree
struct_tree = parse_structure_tree(pdf)
# Walk tree in reading order
for element in struct_tree:
if element.type == 'H1':
print(f"[Heading Level 1] {element.text}")
elif element.type == 'P':
print(f"[Paragraph] {element.text}")
elif element.type == 'Figure':
alt_text = element.get_alt_text()
print(f"[Image] {alt_text or 'NO ALT TEXT'}")
elif element.type == 'Table':
print(f"[Table with {rows} rows, {cols} columns]")
```
**Tools needed:**
- pypdf for structure tree parsing
- Custom tree walker
- Tag-to-announcement mapping
**Time to build:** 2-3 days
**Value:** High - shows exact reading order issues
---
### 2. Reading Order Validator ✅ FEASIBLE
**What it does:**
- Compare visual order vs. tag order
- Detect reading order problems
- Flag if content reads incorrectly
**Example issues it would catch:**
```
Visual layout:
┌─────────────┬─────────────┐
│ Column 1 │ Column 2 │
│ Paragraph A │ Paragraph C │
│ Paragraph B │ Paragraph D │
└─────────────┴─────────────┘
Tag order (what SR reads):
1. Column 1 Paragraph A
2. Column 1 Paragraph B
3. Column 2 Paragraph C ← WRONG! Should be #2
4. Column 2 Paragraph D
ISSUE: Multi-column layout not properly tagged!
```
**Time to build:** 3-4 days
**Value:** Medium-High - catches common layout issues
---
### 3. Accessibility Tree Inspector ✅ FEASIBLE
**What it does:**
- Show PDF accessibility tree (like Chrome DevTools)
- Display all accessible properties
- Highlight missing names/roles/values
**Visual output:**
```
Document
├─ Article
│ ├─ H1 "Annual Report" ✅
│ ├─ P "This year we..." ✅
│ ├─ Figure [NO ALT TEXT] ❌
│ └─ Table
│ ├─ TR (header=true) ✅
│ └─ TR (header=false) ✅
└─ Form
├─ Field "email" (tooltip="Email Address") ✅
└─ Field "phone" (NO TOOLTIP) ❌
```
**Time to build:** 4-5 days
**Value:** High - visual debugging tool
---
## What We CANNOT Build (Unrealistic)
### ❌ Full Screen Reader
**Why not:**
- Requires OS-level hooks (Windows MSAA/UIA, macOS Accessibility API)
- Need TTS (Text-to-Speech) engine integration
- Complex rendering pipeline
- Must support ALL applications, not just PDFs
- Years of development, 100,000+ lines of code
**Equivalent effort:** Building a web browser from scratch
---
### ❌ Real-Time Audio Output
**Why not:**
- Need professional TTS engine (expensive licensing)
- Voice customization
- Speech rate controls
- Pronunciation dictionaries
- Multi-language support
**Better alternative:** Use existing screen readers (NVDA is free!)
---
## ⌨️ Keyboard Navigation Testing
### What We COULD Build (Partially)
#### 1. Tab Order Validator ✅ FEASIBLE
**What it does:**
- Extract tab order from PDF form fields
- Detect if tab indices are set
- Flag fields with no tab order
- Verify tab order is logical (1, 2, 3... not 1, 5, 2, 8)
**Code example:**
```python
def check_tab_order(pdf):
form_fields = get_form_fields(pdf)
for field in form_fields:
tab_index = field.get('/T') # Tab index
if not tab_index:
issue("Field has no tab order")
# Check for gaps/skips
indices = sorted([f.tab_index for f in form_fields])
for i, idx in enumerate(indices):
if i > 0 and idx != indices[i-1] + 1:
issue(f"Tab order jumps from {indices[i-1]} to {idx}")
```
**Time to build:** 1-2 days
**Value:** Medium - catches common form issues
---
#### 2. Focus Order Detection ✅ FEASIBLE
**What it does:**
- Map visual position of form fields
- Compare to programmatic tab order
- Detect if focus jumps around illogically
**Example:**
```
Visual layout: Tab order:
┌─────────┐ 1. Name ✅
│ Name │ 1 2. Email ✅
│ Email │ 2 3. Submit ❌ WRONG! Should be #4
│ Phone │ 4 4. Phone ❌ WRONG! Should be #3
│ Submit │ 3
└─────────┘
ISSUE: Tab order doesn't match visual layout!
```
**Time to build:** 2-3 days
**Value:** Medium - useful for complex forms
---
### What We CANNOT Build
#### ❌ Actual Keyboard Navigation Simulation
**Why not:**
- Need to launch PDF reader (Adobe, Preview, etc.)
- Simulate keyboard input (requires automation framework)
- Capture behavior (focus changes, interactions)
- Different readers behave differently
- Slow and brittle
**What this would require:**
1. Launch PDF in Adobe Acrobat
2. Use Selenium/Playwright to send keyboard events
3. Monitor focus changes
4. Detect keyboard traps
5. Verify all functionality accessible
**Problems:**
- Adobe Acrobat not automation-friendly
- Each PDF reader has different keyboard shortcuts
- Slow (30+ seconds per test)
- Flaky (automation breaks with UI changes)
- Requires GUI (can't run headless)
**Better solution:** Manual testing with actual keyboard
---
## 💡 **Recommended Approach**
### Build What's Useful:
**Phase 1 (High Value, Quick Wins):**
1. ✅ **Screen Reader Output Simulator** (3 days)
- Show what SR would announce
- Detect reading order issues
- Most valuable feature
2. ✅ **Tab Order Validator** (2 days)
- Check form field tab order
- Detect missing tab indices
- Quick win for forms
**Phase 2 (Medium Value):**
3. ⚠️ **Accessibility Tree Inspector** (4 days)
- Visual tree viewer
- Helpful for debugging
4. ⚠️ **Focus Order Detector** (3 days)
- Compare visual vs. programmatic order
- Useful for complex forms
**Don't Build (Not Worth It):**
- ❌ Full screen reader (months of work, low ROI)
- ❌ TTS integration (expensive, existing solutions better)
- ❌ Keyboard automation (brittle, slow, limited value)
---
## 🚀 **My Recommendation**
### **Option A: Build Screen Reader Simulator** (Best ROI)
**Effort:** 3-4 days
**Value:** HIGH
**What you get:**
```
📄 Screen Reader Preview
─────────────────────────────
[Document Title] "Annual Report 2024"
[Heading 1] "Executive Summary"
[Paragraph] "This year saw significant growth..."
[Image] NO ALT TEXT ❌
[Heading 2] "Financial Results"
[Table: 4 columns, 10 rows]
[Row 1, Header] "Quarter" "Revenue" "Profit" "Growth"
[Row 2] "Q1" "$1.2M" "$150K" "12%"
...
```
**Benefits:**
- Shows EXACTLY what blind users hear
- Catches reading order problems
- Validates alt text presence
- No need for actual screen reader
- Works in web interface
**This would be VERY valuable!**
---
### **Option B: Add Tab Order Checking** (Quick Win)
**Effort:** 1-2 days
**Value:** MEDIUM
**What you get:**
- ✅ Verify tab order exists
- ✅ Detect illogical tab sequences
- ✅ Flag forms with no tab order
- ⚠️ Can't test actual behavior (still need manual)
---
### **Option C: Do Nothing** (Use Existing Tools)
**Free screen readers:**
- NVDA (Windows) - Free, excellent
- VoiceOver (Mac) - Built-in
- JAWS (Windows) - Commercial, industry standard
**Recommendation:** Train users to test with NVDA (5 minutes to learn)
**Keyboard testing:** Just manually test (Tab through the PDF)
---
## 🎯 **My Suggestion:**
### **Build the Screen Reader Simulator**
**Why:**
1. **High value** - Shows reading order issues (common problem)
2. **Unique feature** - Competitors don't have this
3. **Fast to build** - 3-4 days with existing code
4. **Integrates well** - Add to Visual Page Inspector
5. **Educational** - Helps users understand accessibility
**What it would show:**
- Text content in SR order
- Image alt text (or "MISSING")
- Table structure
- Heading hierarchy
- Form field labels
- Link text
**How it helps:**
- Catch reading order bugs without screen reader
- Verify alt text before publishing
- Educational for non-technical users
- Great demo feature
---
## ❓ **Want Me To Build It?**
I can build a **Screen Reader Output Simulator** that:
- Parses PDF structure tree
- Simulates screen reader announcements
- Shows reading order issues
- Displays in web interface
- Highlights problems visually
**Estimated time:** 3-4 days of development
**Would you like me to:**
1. ✅ Build the Screen Reader Simulator (high value)
2. ⚠️ Build Tab Order Validator (quick win, lower value)
3. ❌ Skip it and use existing screen readers (practical approach)
What do you think? The Screen Reader Simulator would be a really cool feature! 🎯

View file

@ -1,275 +0,0 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (ReportLab PDF Library \055 www\056reportlab\056com)
/Author (anonymous)
/CreationDate (D\07220251020161349\05504\04700\047)
/Creator (ReportLab PDF Library \055 www\056reportlab\056com)
/Keywords ()
/ModDate (D\07220251020161349\05504\04700\047)
/Subject (unspecified)
/Title (untitled)
/Trapped (\057False)
>>
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<Hg:Y-'n#Xrd8=7TiGM"0G'\HB?`YZN(lJP1Nn<o@lRg/V'H5\cXLWQe5!HU8*Re2Z'rnZ@:sJ/>HT`hpOU*nK9/qZ*Zp?=GnqpB^3Zg\lWZTo68Cf!.WaZc`5in9GDZ%R(!@*)"BsDt<AuYIWQc+ns`3FKk/3P![CZplDX#&*C#u/GnVu^(3)n,O=E=1orRgOGl#P9O=Gh+\K90X1KCIpC'cT[(dJIdRo`IU_IC8%(.j!C^d9i`=VAP6Y9rsUsP`DLoE7j?<cPm=s6^fP\i`S;Np$AJa*p4#]m6~>
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[,2<I!lREY+!s53kE^ANGls8Tf]-Bm+N6psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF2ru7_'0//kii9d)4WUf\/P`t-fWn>rHrJ#asCm5A2"&B_B^UJ.5Pg)(W4tUjAf'D)"GAH+82g'Isrrd%Tku'ZgpDf*>*^&'j%Alo!_-k#Hm)R^:BuZ,#j5QM<A5pRB?GHJOA7TAgI_V1!pVc1n8h.3@TNI-"W&JJ@6Amu`DZ$t#kgF%?VQ+_#>uHrS=0cl.$r(S`p^gCfHs!XaaZN9thnJDf_ha+TerJNh*iU_n0Nr1o`'5C=/bZ0)s,@upTEO@Flpm!P1EX/;nPE.^HpU/o>TODT3(;.<AANm'Pr(cWQ7j>]Cu2M]Akd,/Jj7EPmL@Y>H0!&eZ;jq+fa8Jn[CBSc,Q1K).J#A=+<K?2&$9%XQ?";NF*$0!][a$YlhbcPNu[EiE#XrL%j,\KHR19qji]m^o1L&^DXQ>m2,O;58\$0Bi`mN;<!\XWL^/Pj&f'!g#kmWLL^#5&I\8.)EMGG7e'bo!GMTh`e5]g]R4hm25WLIER]Yl)q$0n*Wq>puBJ<i00,AbH/WW<adb2aa[Er=#MEt.7`;buHhl+`kB52'#3Rgi,fO!6Gb*6W:p;e\nWouZ7.MeP;7l!NMoiXH!Y@%;R$BYq<LG-V5C23DS-!i"-*BNPN\AGIHe(_6D;c(2B;t$PULLVJg\u!B:)Wq]KhV8bR%NK.0X%N<epnT%O0[spgk`!J:[53m1mft4hnR?p2@+JrWBU^pY9=i)obG0Y/jchl*VF[gmrLjq"4\F_o")tM6Y\@!Ik0+,[aisD*9TB[)2fHE]Wcmb>":)t<-J#>J6bcQhH*h^0%lD(/=]OH'\&."82dmjZ.`C>7g6kJ)pX?"an$5N;#3QFZB?@PQPGYrS.`bI^aWkASU`Qna<jQG"a"iB"=IqMB-`-OhYneb@]t9K*.g\5[(J9s=Ngr^6o#9nTaZo'7C7Ie]-/H-')B+PS\O]?BnW24fQs_Ihn%MMGVY928Sc-Vuj7;<C0)p.E9B)0u1)3KF%NYC6<Y<>S=_3k4rq,H=Y^H*,7oG8e96PJmMg]%oL[t94a2mP93T"<=b*@2CHaK)/<N=hE11FUrTr7&u.G)Lf@,PbSl#?+/Tk_m&TffWY+,heV\n0&t0)p.E9B*$8Ot"hS8"R5O@'sk+KCT!L.>-0:/YckY<O(ONXL;e^9L;T4ZTtX'?U-lhPUIcrB$L>)m*Xs:n(?88?f*-*]dE_ec'g:C2nME;OZiZ53qY[;QRs0Anp`U3,gOOW-/dn,mD=RPe8p"]pDftG9"K3%J^k&?An!bFUU'a<!t6%[Nq.i+Is'_H9D)*u*^2uu8"4dWad7=`V2tZAePgHeNus^l=nB)u9HDA&S,Jj=pE!?O0-6fIKcN7dl44isjmo>m7l`)\PUY%:&W9?e;eG^SPk'ORW`<D9H/H=G=PgHdZ_eD(7ZAKS>@!%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[4Zo<A^&#/mDCflN)>CIdI:%'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(5G<F9M[`.*CDkL'=c]/>TjDdJYj1?`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[M<g<Xbzzzzzzzzzzzzzzzzzz!3#KQhP/$PWtnE/7YZ6JmdqSsNGZBuaYr[hoH+CbMs8jW"W_Z0qN&SO+8_>V^><!?7L9aGiB:36<aR>/De,dTs"dW/n=tYn@@IYt^3f"@Ih/A?Y]VGp81uG[peeoHYgio'hm`&MIoP`;r/k<j=`#c!V-O^Ah.5(#,1Rr/okLDu0@G?8`o9S$Z!k)PUpXA^;>knsZL?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<HY?0Cq*I\i-jtW=4!0<ul@qh'Vf;'o*UWk`#1N)&[24oLL'fr&5@hr!lI,3R.cr=ii;RD>%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'k<dXSlt<"1W_<I2JoD8(9!itST`nkfe9f",8"sjPfIeGqIZ).HHFI^4l7Z-bq:MF\'+;h^K:A?Y0%RGA!ZN0H'&mOF#RMlMRf5OBQKsqA8oC:T4JFJ5(U)27A*a+Q/ZA_BDIQ&4,qDk?+[RaV&PI03DW@\OR8<B=1>ThlUJEQ1tSl<L?kb#Y+AjVV5&0[cZ[T4PPh_O]$nPU1S7e3SVV8k+5QqcXkqauS+#Wco@:ELU60\bTJ9,$8o/&E)4WD(B,O(+b$[5f5d<X*`QRPT/R5dJT5Se:n!sojh$8)QNI0eI:JGEae'U77S-[>M_cum"<&&$L_map'IJT$]MO\$'cR$?=G<FPis.2APU&&r\4lsTu4hJ@mY1\XP1NiT9?8WTH?4:[?nPk84_$RGqQ8)':)=1uJ-rrm8GZd2@\#Z"R'U\9C]rq&Ph-E$N.RR(,cTjYaoUcUG<pssK?:sWC@_9$cVZ12PG:*,3HTcci]OrP&hVFiKP\XmAf=pn4`uUbo?p:ZM-3kl%5o6S!/7W?LMPi4_%o/MRZ]&>@b$[Gl5d<X*`QRNc#Iq?FVq,SV>5M?TNRN7Z)Ht[4f51-X?2?jF-N;'7m:-%G"'$G=S)fXD\;g6SI<pT2ogE`/c1M>%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`\;+<Yb+@W=<WrEUG@df\S%-@b,G.>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++Y1kCljHohQdV<decZl<<?`@a5PXaVK;YH"*gQ4lfN4]a(*GnWI7"=ACo_4aDD8X0,koFA(5olHOZul@-67O"73d-sO0a*q*@eg?50u-t-TK4%a=##T9-db@_\[hoL$lKB4Wc`<rSD)jN__D:qm6[UirR4')Bq-$kbJd'<h;54OeC'Qf2uA^4PDbRLnl0.?"\S[4j,k1;JAnh>6O0JW2?-+5R^$r32OZ](SrA7C$/D)7*C.tX"bNQSJCZ;,PaW7K48VY08N^RL6(qH1#:[Zn7US:L06WbDRKs)OL"1.Y3O2_eCKeaM2O-2O^p3(MRHGp$`VC&G)<?dm./dJm6TR>8MOe2W2sU\IlE0Yn(%I$QMZNK!=U<$e)(ckSi0<F$KjIO"pY%OqR2=B#J)Z)A'2@Sn!Czzzzzzzzzzzzzzzzzz!%ICZ[=\bf~>
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(<BUV5oP5_)H'2LKKEZjcgQ:2kA?a"F7-S[hR>5Ke+`K+F5HMYqn-F1kFQb_3ELetzzzzzzzzzzzzz!!!!-Pbh%7gc9ZY>2%[UT9kiZ\T1,>XXXaK2c%;%cCBBH/t-eFe<mAeVosi+]%+I7b*DM-b`:YfI!O-84F*Y%-VsFQr:*-H0'CR\NW8W"5/tgK[8jA:QSiOco;5659H$*"/mTr1!2ad+N;+?^WWt1`eAs^qgsZF`4ZI;I]RR,S3]^`m]A%l1@!&BKo1FT#))=VJh:"U\T0IATJlGalfWd2RW6Ce_a,eF4hn&('/ZG\QnX_n22I0.l#L2PGG<4U6.I5S59uF:B2f.^DIqI>c]6>'O%iZi'(<GmtFrDl3\>MUc*'J&[3496qX)6O+hJ>\EHSB<JTL]SgR3HS,l<m\[mZfno!UmjdH)pc9;$5$6\8r!fbf#@dhQA_Tlr_Z&X&h0OWQD=>oUnR_+Kbqs:Y#oqn6ih=9Bg/T8Il`+05Eg?K6mr9bhg$:!;X9d+($j:okI^Hj2U>`:CfL^$[VL(Ue1.BQ#4<Rg\UPXQ;8$GmkEm2k7l")qKa`D]6@3?imWMil%5KiR*/'BZX)CpX08^'-9Z%F$9s$kJ=7DN'Zc[2J9'pSMHtUUcll[Oj2-N@ie@@,_1NH*d+s=#Q[\59#nusFao2+Jp!"En4k`%&1?Qar/V&HY;s`MmK+@.?)-^<loLn*-f!>Sp(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=+(<L'``oUnR_BZB71_gVFKi6ImiV_R*m(n*\H:1NZppCt]9RMmc.[^O&n_kOSWeX6&R))Y#fI<s6`>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<Z$YljiKsZHzzzzzzzzzzzz!!!#uU&P*!Ym<5~>
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\<JO;dfCQ.`%,EoCmegVsbP!=Mc`G;((Yn>1Qa2([\Q]!WE`n%$X:JH`.Hf-pkQ$@Cla,]7W#ls#_nR4E*JhDk=_^$67ImA%Q*jsPZo%EU?hs^V7pj<NOZm%5MqJmoO$9RiKHYuq0^nElfkHXT8XFKN@qaXQN\E!LHUiC_3i]FET&;g.W3)1d1"=S+n8[A2F(L-F.Ku$R@fOE28"Clp73qTFm?*sJc':DFl[;iG4m"I]K!Bq3f]8gG*#nAs!#$8lAV\2u`,r9LgJs[G=T"i-1Y#FtfJZfU2%ZNuK@_U=Z)W#)El!dM?glq?TK9+N;`TTf@bnVM]9k*1KK,C>9XrAn9mOn#o+Z#1X./oD1%_XGSa;L)/*tl3eRO)Igg9(c=9P?3YHHNu1Rbk[:LU).nsp'X5g\g>O2i<mVD"M-f'OEjhf'h/L='PMCjGBF@rb,kA,kDdHcdEV>l4>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<n3*T(@Bbp!a[\kh\W#4jP^]uA?P8t`MX&JAE@;l74aT@%?7Y`]]054#AViMGrk_G&-\u[:5PQVF*/]"KNMoEYHOs23I!XLqt4X67(KB->\P6<pDA62SVg;,b!)ZRVW/jbXa+Z`5^](ir+(k53+>mk=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@<j1We3^?ZF3-&BU=n4cuU?P0!Kd$Da)b+lm+LBY?:9-:&c-V%N6,k-'$EUek'.jVDDMll(JBA!m1,NZ*C1$\;]6WGci0oq1+f-(*<a=d$f,_qa;]7ici[hN&JCi0,fGdOF[=V80<i-g/g^!U@QQ[)>/4RI=sXK:J,?`0/>^^Hh!HrBo2g!<pV1X'$oWLb!8)6J=h,Nb+co-e3#]Er%1Zd<Wajrp*Z:8XS0f'r#nmfshA0H0GN$@3`R*9"!![$E49K?ZR(%k8[2`O]d7.m"8+4=iPTl[ZcU&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J<DAoagkMd>.~>
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%;FUMXjbi<oRt;6`JteWSh_ldoLWH<$lM\@AK?:tJ-25,kSGRj7rrniJfjq!-D18.[*q-eGJ)B@ZY+s7"u7feGBC[VF?m6DaTp[#C',>Ibd15JIF6*n6:0m/6bTml(+Ao=Jqu5.,DqJjNOUDtnEFXN^LjQ06>W09KcCq!g^!*7RRFC<u%`^SLc[/^r#T%1QL$G'.rlO/m:1$*0q[7[rl(^Mdt+eJ+c/HtR*Tm-Le\:RjCQF^it-2Q3uAA`r-rP8a-qm[7Dk4ABZBH2-l;;cAkmeDB&"j&7=hEnj37orIoaH'LL:n6j:s*Vp%G[r/m!j+]EREo]bk^#@WfY&*parp<m_&)P^]U!5/n[TpRrh0,X\>C'@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<m`B7D@%0_nmF`KTQ]tk5<(:WV:t07,2KdoQ9r0:Mk:-5Wi/%TW-bjD*PGUF:fs:G+Z"$AG%Hf\&O4=ciIC-E4FR7m(SfQh5Q=!p@<-%6OV:_!`KPOM';HJ3'8,agr2uH+"3I^n9b$Vo4D4A5P]f*%M^4!%$`go28\iW^0n&q%N,F*]JX+d^g6dOeIo@r'UC_c7#lJ1O1kd;_DB5`$<Lb$C>=j()&k#J/QGIOk`QgCc'fWOpdNr2Pmn*&tKUo',R?+OlO)&X>2$;V3h1GbJJ>$>*]-7Sb5T31pM=$t7[Lm2h2P)^L,n,E:_p%,Y2hdW)09PRM=B5`$<LatiAIAUhmLQ1\9s5qD;V#7eaE^sqSq)Qa>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*<e?nnj_SUo<oTdqVT$<CIRHNJLaiX,E(HX3#/]s!kVad>=[.(Q4p/j6N<&A;c=TmgJc't011du.7e]Q@ted1j$dEuCQH@("(<ZP7#ZX:Ir%>QS0\<6^Wfs<'91?d*Yrl3mSTZ+%D\[e`snF)HpD#)TdT:;<KiQ0)2;cAmnJ8t;L=`p&<042G`hUS4BOaiejWtfI?'hqf8=L9HR`g4$kfe@,:(&iV1,#$I*GB\9,kP$@MT0Mcc=3Kri)`OW6f6r-(GW-j@i=3Q%iLaK'%Z/:)rhSi6Pq><=OC/NZda^P+Oajdos0fAEWg`(adP<,I^V;uQ,2'A>fD,-N%IAuJeO7d5e"ckTDd&(U]mEh-;jtkSs"A%krSkeSq>#<dS=#\REofpSPlpcjZ2_S3@B9X=-6qnjH?ra2&2aktW9XB6VaDYCq>Z/g`^Y*/:0Yce<C4%)h>RXW]%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[<U7o;C2'm*S(3g?[8s>/XdQTi]!gmb[^Idi+ta!1:qXS4:d@8L'MpPrJg`9]G_&*oj[B;t]5lk:?7t$ILI[@8kAi3\CIb/gh.Q)Ek</Lok<AWechX,Q0jS?G&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J=7Cogk<ooF^UY`+:%758<isS9sUbYU%%h\*1l-06V]]CH%]2VJFo/2O&LQFs,_YQ':p/VdeHX*kT2EIG&9.^M`nEI:RR*Is]prrFYn#fWG+UG,:[DKPCJ$R=8l=\+R45gTY'gD"6j*nR)JXFWU"Y\(ImSZt%<DqOH2dJnb6d9I]?p[fiB5^p.n#3Fk?iYM<;q?=9A/kN:ATf4<C`)JnVO4)M%AWCpE`A=@MC6e:fZ]P)Raq5RY#Psgr1[eA;6etG7qa#p^FrDr!/Sc@Q=pU4/4]D>gc>*2Vn1,m/Gn3kt8li['\Hn5te(OPl/pWGa:.L7N>_;@'9@<[JIfm!Y;Fq#iQ>*W-"?9%?^H5lQWk=<Lu)bGP5ObE.$h3ueCl2fsEdh>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#<J:\\i(1$E%%S1a)KC;H17f>'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!q9<I-_q%%:)GIc*FJ_4uX).EJF?3I[[T[Q%?<<*Z`kk3Q-1/B&q1trKafIf*XtP!U<=p/r^rsb%=JQsubg/'+G&+AuNQ3q!o.`f/Sd^nm<Yi28.jDM+Nm_T'*-N1B#ah.UZKO'F2A\=L>s7W0<kQ7iQ03N*>Q=V70^V?XR<HLKI8KQNg;E@e([#RuX?N6D2q.4hko':A'<sh`Th1SA]4V_=o5+uk:]>gka/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<U(MaWMMk^R($_>]3UQ)WXLrhTkAL1Nqp](e_I6for/>(<NMIrhW+k(O4lk$Jjm<a%SE6l$kPB!(2UAaW(-Ef.<N/uep?`qNBl?2jm,PcmdOm/<;::9Nm&u[`!u6_rQ.)>/,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%$.>k<VW/CgHi*j`d/TtPibJgBfD4#X&RhfV%+)MF!"3kM]_@G]qhc<gT0(g:A`ZqOaL)Vk-@Z3<$YGAS)gs:&lK-"rl'-,5*M9ZU"qTa'"@_N%9r#nG[fBdUbhC,+\E4!Ehl-FX!ID,=L8V2%,a`PBpiBBpXPO9:8Mi4hq9Jc`Se+-(0e#sAo0W$I2iVDDl'D%1j:).pF::q\nUk@]<`:?.)UEC>OVK7@+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`nK2<aBfXW76cW@Xn6.)UI3TAg)YU-,:S@1@Y@,oZp1Ih%l$8;+t<Qm9SWZt1Rmdq!uZh:C#@kaEJQ#g*-FO3u80@>oG>q4iWhFc1hYI4r'_j8bX;T\rNki)>`]lI15^[ObkfsST8VodBK%7U*+4ust^O'%Jk&hHsIW1DRX-QC5H*H?@\rGCjBpH>n<pFV"SO'[^q#?LST4n2!.,#"X2_L!\h,(tfsFPG7;rAVi!7GdY`jEnI,#ZXm%9V`O4h'ntl%(?h6^"W)t.%GYckaT]4~>
endstream
endobj
xref
0 21
0000000000 65535 f
0000000015 00000 n
0000000355 00000 n
0000000428 00000 n
0000000494 00000 n
0000000845 00000 n
0000001277 00000 n
0000001339 00000 n
0000001446 00000 n
0000001558 00000 n
0000001641 00000 n
0000001719 00000 n
0000004457 00000 n
0000006910 00000 n
0000008551 00000 n
0000008904 00000 n
0000009340 00000 n
0000011289 00000 n
0000013577 00000 n
0000016036 00000 n
0000016227 00000 n
trailer
<<
/Size 21
/Root 3 0 R
/Info 1 0 R
>>
startxref
16761
%%EOF

View file

@ -1,275 +0,0 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (ReportLab PDF Library \055 www\056reportlab\056com)
/Author (anonymous)
/CreationDate (D\07220251020161349\05504\04700\047)
/Creator (ReportLab PDF Library \055 www\056reportlab\056com)
/Keywords ()
/ModDate (D\07220251020161349\05504\04700\047)
/Subject (unspecified)
/Title (untitled)
/Trapped (\057False)
>>
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<Hg:Y-'n#Xrd8=7TiGM"0G'\HB?`YZN(lJP1Nn<o@lRg/V'H5\cXLWQe5!HU8*Re2Z'rnZ@:sJ/>HT`hpOU*nK9/qZ*Zp?=GnqpB^3Zg\lWZTo68Cf!.WaZc`5in9GDZ%R(!@*)"BsDt<AuYIWQc+ns`3FKk/3P![CZplDX#&*C#u/GnVu^(3)n,O=E=1orRgOGl#P9O=Gh+\K90X1KCIpC'cT[(dJIdRo`IU_IC8%(.j!C^d9i`=VAP6Y9rsUsP`DLoE7j?<cPm=s6^fP\i`S;Np$AJa*p4#]m6~>
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[,2<I!lREY+!s53kE^ANGls8Tf]-Bm+N6psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF2ru7_'0//kii9d)4WUf\/P`t-fWn>rHrJ#asCm5A2"&B_B^UJ.5Pg)(W4tUjAf'D)"GAH+82g'Isrrd%Tku'ZgpDf*>*^&'j%Alo!_-k#Hm)R^:BuZ,#j5QM<A5pRB?GHJOA7TAgI_V1!pVc1n8h.3@TNI-"W&JJ@6Amu`DZ$t#kgF%?VQ+_#>uHrS=0cl.$r(S`p^gCfHs!XaaZN9thnJDf_ha+TerJNh*iU_n0Nr1o`'5C=/bZ0)s,@upTEO@Flpm!P1EX/;nPE.^HpU/o>TODT3(;.<AANm'Pr(cWQ7j>]Cu2M]Akd,/Jj7EPmL@Y>H0!&eZ;jq+fa8Jn[CBSc,Q1K).J#A=+<K?2&$9%XQ?";NF*$0!][a$YlhbcPNu[EiE#XrL%j,\KHR19qji]m^o1L&^DXQ>m2,O;58\$0Bi`mN;<!\XWL^/Pj&f'!g#kmWLL^#5&I\8.)EMGG7e'bo!GMTh`e5]g]R4hm25WLIER]Yl)q$0n*Wq>puBJ<i00,AbH/WW<adb2aa[Er=#MEt.7`;buHhl+`kB52'#3Rgi,fO!6Gb*6W:p;e\nWouZ7.MeP;7l!NMoiXH!Y@%;R$BYq<LG-V5C23DS-!i"-*BNPN\AGIHe(_6D;c(2B;t$PULLVJg\u!B:)Wq]KhV8bR%NK.0X%N<epnT%O0[spgk`!J:[53m1mft4hnR?p2@+JrWBU^pY9=i)obG0Y/jchl*VF[gmrLjq"4\F_o")tM6Y\@!Ik0+,[aisD*9TB[)2fHE]Wcmb>":)t<-J#>J6bcQhH*h^0%lD(/=]OH'\&."82dmjZ.`C>7g6kJ)pX?"an$5N;#3QFZB?@PQPGYrS.`bI^aWkASU`Qna<jQG"a"iB"=IqMB-`-OhYneb@]t9K*.g\5[(J9s=Ngr^6o#9nTaZo'7C7Ie]-/H-')B+PS\O]?BnW24fQs_Ihn%MMGVY928Sc-Vuj7;<C0)p.E9B)0u1)3KF%NYC6<Y<>S=_3k4rq,H=Y^H*,7oG8e96PJmMg]%oL[t94a2mP93T"<=b*@2CHaK)/<N=hE11FUrTr7&u.G)Lf@,PbSl#?+/Tk_m&TffWY+,heV\n0&t0)p.E9B*$8Ot"hS8"R5O@'sk+KCT!L.>-0:/YckY<O(ONXL;e^9L;T4ZTtX'?U-lhPUIcrB$L>)m*Xs:n(?88?f*-*]dE_ec'g:C2nME;OZiZ53qY[;QRs0Anp`U3,gOOW-/dn,mD=RPe8p"]pDftG9"K3%J^k&?An!bFUU'a<!t6%[Nq.i+Is'_H9D)*u*^2uu8"4dWad7=`V2tZAePgHeNus^l=nB)u9HDA&S,Jj=pE!?O0-6fIKcN7dl44isjmo>m7l`)\PUY%:&W9?e;eG^SPk'ORW`<D9H/H=G=PgHdZ_eD(7ZAKS>@!%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[4Zo<A^&#/mDCflN)>CIdI:%'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(5G<F9M[`.*CDkL'=c]/>TjDdJYj1?`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[M<g<Xbzzzzzzzzzzzzzzzzzz!3#KQhP/$PWtnE/7YZ6JmdqSsNGZBuaYr[hoH+CbMs8jW"W_Z0qN&SO+8_>V^><!?7L9aGiB:36<aR>/De,dTs"dW/n=tYn@@IYt^3f"@Ih/A?Y]VGp81uG[peeoHYgio'hm`&MIoP`;r/k<j=`#c!V-O^Ah.5(#,1Rr/okLDu0@G?8`o9S$Z!k)PUpXA^;>knsZL?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<HY?0Cq*I\i-jtW=4!0<ul@qh'Vf;'o*UWk`#1N)&[24oLL'fr&5@hr!lI,3R.cr=ii;RD>%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'k<dXSlt<"1W_<I2JoD8(9!itST`nkfe9f",8"sjPfIeGqIZ).HHFI^4l7Z-bq:MF\'+;h^K:A?Y0%RGA!ZN0H'&mOF#RMlMRf5OBQKsqA8oC:T4JFJ5(U)27A*a+Q/ZA_BDIQ&4,qDk?+[RaV&PI03DW@\OR8<B=1>ThlUJEQ1tSl<L?kb#Y+AjVV5&0[cZ[T4PPh_O]$nPU1S7e3SVV8k+5QqcXkqauS+#Wco@:ELU60\bTJ9,$8o/&E)4WD(B,O(+b$[5f5d<X*`QRPT/R5dJT5Se:n!sojh$8)QNI0eI:JGEae'U77S-[>M_cum"<&&$L_map'IJT$]MO\$'cR$?=G<FPis.2APU&&r\4lsTu4hJ@mY1\XP1NiT9?8WTH?4:[?nPk84_$RGqQ8)':)=1uJ-rrm8GZd2@\#Z"R'U\9C]rq&Ph-E$N.RR(,cTjYaoUcUG<pssK?:sWC@_9$cVZ12PG:*,3HTcci]OrP&hVFiKP\XmAf=pn4`uUbo?p:ZM-3kl%5o6S!/7W?LMPi4_%o/MRZ]&>@b$[Gl5d<X*`QRNc#Iq?FVq,SV>5M?TNRN7Z)Ht[4f51-X?2?jF-N;'7m:-%G"'$G=S)fXD\;g6SI<pT2ogE`/c1M>%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`\;+<Yb+@W=<WrEUG@df\S%-@b,G.>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++Y1kCljHohQdV<decZl<<?`@a5PXaVK;YH"*gQ4lfN4]a(*GnWI7"=ACo_4aDD8X0,koFA(5olHOZul@-67O"73d-sO0a*q*@eg?50u-t-TK4%a=##T9-db@_\[hoL$lKB4Wc`<rSD)jN__D:qm6[UirR4')Bq-$kbJd'<h;54OeC'Qf2uA^4PDbRLnl0.?"\S[4j,k1;JAnh>6O0JW2?-+5R^$r32OZ](SrA7C$/D)7*C.tX"bNQSJCZ;,PaW7K48VY08N^RL6(qH1#:[Zn7US:L06WbDRKs)OL"1.Y3O2_eCKeaM2O-2O^p3(MRHGp$`VC&G)<?dm./dJm6TR>8MOe2W2sU\IlE0Yn(%I$QMZNK!=U<$e)(ckSi0<F$KjIO"pY%OqR2=B#J)Z)A'2@Sn!Czzzzzzzzzzzzzzzzzz!%ICZ[=\bf~>
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(<BUV5oP5_)H'2LKKEZjcgQ:2kA?a"F7-S[hR>5Ke+`K+F5HMYqn-F1kFQb_3ELetzzzzzzzzzzzzz!!!!-Pbh%7gc9ZY>2%[UT9kiZ\T1,>XXXaK2c%;%cCBBH/t-eFe<mAeVosi+]%+I7b*DM-b`:YfI!O-84F*Y%-VsFQr:*-H0'CR\NW8W"5/tgK[8jA:QSiOco;5659H$*"/mTr1!2ad+N;+?^WWt1`eAs^qgsZF`4ZI;I]RR,S3]^`m]A%l1@!&BKo1FT#))=VJh:"U\T0IATJlGalfWd2RW6Ce_a,eF4hn&('/ZG\QnX_n22I0.l#L2PGG<4U6.I5S59uF:B2f.^DIqI>c]6>'O%iZi'(<GmtFrDl3\>MUc*'J&[3496qX)6O+hJ>\EHSB<JTL]SgR3HS,l<m\[mZfno!UmjdH)pc9;$5$6\8r!fbf#@dhQA_Tlr_Z&X&h0OWQD=>oUnR_+Kbqs:Y#oqn6ih=9Bg/T8Il`+05Eg?K6mr9bhg$:!;X9d+($j:okI^Hj2U>`:CfL^$[VL(Ue1.BQ#4<Rg\UPXQ;8$GmkEm2k7l")qKa`D]6@3?imWMil%5KiR*/'BZX)CpX08^'-9Z%F$9s$kJ=7DN'Zc[2J9'pSMHtUUcll[Oj2-N@ie@@,_1NH*d+s=#Q[\59#nusFao2+Jp!"En4k`%&1?Qar/V&HY;s`MmK+@.?)-^<loLn*-f!>Sp(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=+(<L'``oUnR_BZB71_gVFKi6ImiV_R*m(n*\H:1NZppCt]9RMmc.[^O&n_kOSWeX6&R))Y#fI<s6`>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<Z$YljiKsZHzzzzzzzzzzzz!!!#uU&P*!Ym<5~>
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\<JO;dfCQ.`%,EoCmegVsbP!=Mc`G;((Yn>1Qa2([\Q]!WE`n%$X:JH`.Hf-pkQ$@Cla,]7W#ls#_nR4E*JhDk=_^$67ImA%Q*jsPZo%EU?hs^V7pj<NOZm%5MqJmoO$9RiKHYuq0^nElfkHXT8XFKN@qaXQN\E!LHUiC_3i]FET&;g.W3)1d1"=S+n8[A2F(L-F.Ku$R@fOE28"Clp73qTFm?*sJc':DFl[;iG4m"I]K!Bq3f]8gG*#nAs!#$8lAV\2u`,r9LgJs[G=T"i-1Y#FtfJZfU2%ZNuK@_U=Z)W#)El!dM?glq?TK9+N;`TTf@bnVM]9k*1KK,C>9XrAn9mOn#o+Z#1X./oD1%_XGSa;L)/*tl3eRO)Igg9(c=9P?3YHHNu1Rbk[:LU).nsp'X5g\g>O2i<mVD"M-f'OEjhf'h/L='PMCjGBF@rb,kA,kDdHcdEV>l4>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<n3*T(@Bbp!a[\kh\W#4jP^]uA?P8t`MX&JAE@;l74aT@%?7Y`]]054#AViMGrk_G&-\u[:5PQVF*/]"KNMoEYHOs23I!XLqt4X67(KB->\P6<pDA62SVg;,b!)ZRVW/jbXa+Z`5^](ir+(k53+>mk=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@<j1We3^?ZF3-&BU=n4cuU?P0!Kd$Da)b+lm+LBY?:9-:&c-V%N6,k-'$EUek'.jVDDMll(JBA!m1,NZ*C1$\;]6WGci0oq1+f-(*<a=d$f,_qa;]7ici[hN&JCi0,fGdOF[=V80<i-g/g^!U@QQ[)>/4RI=sXK:J,?`0/>^^Hh!HrBo2g!<pV1X'$oWLb!8)6J=h,Nb+co-e3#]Er%1Zd<Wajrp*Z:8XS0f'r#nmfshA0H0GN$@3`R*9"!![$E49K?ZR(%k8[2`O]d7.m"8+4=iPTl[ZcU&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J<DAoagkMd>.~>
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%;FUMXjbi<oRt;6`JteWSh_ldoLWH<$lM\@AK?:tJ-25,kSGRj7rrniJfjq!-D18.[*q-eGJ)B@ZY+s7"u7feGBC[VF?m6DaTp[#C',>Ibd15JIF6*n6:0m/6bTml(+Ao=Jqu5.,DqJjNOUDtnEFXN^LjQ06>W09KcCq!g^!*7RRFC<u%`^SLc[/^r#T%1QL$G'.rlO/m:1$*0q[7[rl(^Mdt+eJ+c/HtR*Tm-Le\:RjCQF^it-2Q3uAA`r-rP8a-qm[7Dk4ABZBH2-l;;cAkmeDB&"j&7=hEnj37orIoaH'LL:n6j:s*Vp%G[r/m!j+]EREo]bk^#@WfY&*parp<m_&)P^]U!5/n[TpRrh0,X\>C'@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<m`B7D@%0_nmF`KTQ]tk5<(:WV:t07,2KdoQ9r0:Mk:-5Wi/%TW-bjD*PGUF:fs:G+Z"$AG%Hf\&O4=ciIC-E4FR7m(SfQh5Q=!p@<-%6OV:_!`KPOM';HJ3'8,agr2uH+"3I^n9b$Vo4D4A5P]f*%M^4!%$`go28\iW^0n&q%N,F*]JX+d^g6dOeIo@r'UC_c7#lJ1O1kd;_DB5`$<Lb$C>=j()&k#J/QGIOk`QgCc'fWOpdNr2Pmn*&tKUo',R?+OlO)&X>2$;V3h1GbJJ>$>*]-7Sb5T31pM=$t7[Lm2h2P)^L,n,E:_p%,Y2hdW)09PRM=B5`$<LatiAIAUhmLQ1\9s5qD;V#7eaE^sqSq)Qa>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*<e?nnj_SUo<oTdqVT$<CIRHNJLaiX,E(HX3#/]s!kVad>=[.(Q4p/j6N<&A;c=TmgJc't011du.7e]Q@ted1j$dEuCQH@("(<ZP7#ZX:Ir%>QS0\<6^Wfs<'91?d*Yrl3mSTZ+%D\[e`snF)HpD#)TdT:;<KiQ0)2;cAmnJ8t;L=`p&<042G`hUS4BOaiejWtfI?'hqf8=L9HR`g4$kfe@,:(&iV1,#$I*GB\9,kP$@MT0Mcc=3Kri)`OW6f6r-(GW-j@i=3Q%iLaK'%Z/:)rhSi6Pq><=OC/NZda^P+Oajdos0fAEWg`(adP<,I^V;uQ,2'A>fD,-N%IAuJeO7d5e"ckTDd&(U]mEh-;jtkSs"A%krSkeSq>#<dS=#\REofpSPlpcjZ2_S3@B9X=-6qnjH?ra2&2aktW9XB6VaDYCq>Z/g`^Y*/:0Yce<C4%)h>RXW]%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[<U7o;C2'm*S(3g?[8s>/XdQTi]!gmb[^Idi+ta!1:qXS4:d@8L'MpPrJg`9]G_&*oj[B;t]5lk:?7t$ILI[@8kAi3\CIb/gh.Q)Ek</Lok<AWechX,Q0jS?G&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J=7Cogk<ooF^UY`+:%758<isS9sUbYU%%h\*1l-06V]]CH%]2VJFo/2O&LQFs,_YQ':p/VdeHX*kT2EIG&9.^M`nEI:RR*Is]prrFYn#fWG+UG,:[DKPCJ$R=8l=\+R45gTY'gD"6j*nR)JXFWU"Y\(ImSZt%<DqOH2dJnb6d9I]?p[fiB5^p.n#3Fk?iYM<;q?=9A/kN:ATf4<C`)JnVO4)M%AWCpE`A=@MC6e:fZ]P)Raq5RY#Psgr1[eA;6etG7qa#p^FrDr!/Sc@Q=pU4/4]D>gc>*2Vn1,m/Gn3kt8li['\Hn5te(OPl/pWGa:.L7N>_;@'9@<[JIfm!Y;Fq#iQ>*W-"?9%?^H5lQWk=<Lu)bGP5ObE.$h3ueCl2fsEdh>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#<J:\\i(1$E%%S1a)KC;H17f>'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!q9<I-_q%%:)GIc*FJ_4uX).EJF?3I[[T[Q%?<<*Z`kk3Q-1/B&q1trKafIf*XtP!U<=p/r^rsb%=JQsubg/'+G&+AuNQ3q!o.`f/Sd^nm<Yi28.jDM+Nm_T'*-N1B#ah.UZKO'F2A\=L>s7W0<kQ7iQ03N*>Q=V70^V?XR<HLKI8KQNg;E@e([#RuX?N6D2q.4hko':A'<sh`Th1SA]4V_=o5+uk:]>gka/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<U(MaWMMk^R($_>]3UQ)WXLrhTkAL1Nqp](e_I6for/>(<NMIrhW+k(O4lk$Jjm<a%SE6l$kPB!(2UAaW(-Ef.<N/uep?`qNBl?2jm,PcmdOm/<;::9Nm&u[`!u6_rQ.)>/,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%$.>k<VW/CgHi*j`d/TtPibJgBfD4#X&RhfV%+)MF!"3kM]_@G]qhc<gT0(g:A`ZqOaL)Vk-@Z3<$YGAS)gs:&lK-"rl'-,5*M9ZU"qTa'"@_N%9r#nG[fBdUbhC,+\E4!Ehl-FX!ID,=L8V2%,a`PBpiBBpXPO9:8Mi4hq9Jc`Se+-(0e#sAo0W$I2iVDDl'D%1j:).pF::q\nUk@]<`:?.)UEC>OVK7@+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`nK2<aBfXW76cW@Xn6.)UI3TAg)YU-,:S@1@Y@,oZp1Ih%l$8;+t<Qm9SWZt1Rmdq!uZh:C#@kaEJQ#g*-FO3u80@>oG>q4iWhFc1hYI4r'_j8bX;T\rNki)>`]lI15^[ObkfsST8VodBK%7U*+4ust^O'%Jk&hHsIW1DRX-QC5H*H?@\rGCjBpH>n<pFV"SO'[^q#?LST4n2!.,#"X2_L!\h,(tfsFPG7;rAVi!7GdY`jEnI,#ZXm%9V`O4h'ntl%(?h6^"W)t.%GYckaT]4~>
endstream
endobj
xref
0 21
0000000000 65535 f
0000000015 00000 n
0000000355 00000 n
0000000428 00000 n
0000000494 00000 n
0000000845 00000 n
0000001277 00000 n
0000001339 00000 n
0000001446 00000 n
0000001558 00000 n
0000001641 00000 n
0000001719 00000 n
0000004457 00000 n
0000006910 00000 n
0000008551 00000 n
0000008904 00000 n
0000009340 00000 n
0000011289 00000 n
0000013577 00000 n
0000016036 00000 n
0000016227 00000 n
trailer
<<
/Size 21
/Root 3 0 R
/Info 1 0 R
>>
startxref
16761
%%EOF

View file

@ -1,49 +0,0 @@
<?php
/**
* Test that PHP can access environment variables
*/
echo "==================================================\n";
echo "PHP Environment Variable Test\n";
echo "==================================================\n\n";
// Check if .env file exists
if (file_exists(__DIR__ . '/.env')) {
echo "✅ .env file exists\n\n";
} else {
echo "❌ .env file not found\n\n";
exit(1);
}
// Note: PHP doesn't automatically load .env files
// Environment variables need to be set in the system or web server config
// OR we need to use a PHP library like vlucas/phpdotenv
echo "Checking environment variables:\n\n";
$anthropic_key = getenv('ANTHROPIC_API_KEY');
if ($anthropic_key) {
echo "✅ ANTHROPIC_API_KEY: " . substr($anthropic_key, 0, 20) . "..." . substr($anthropic_key, -10) . "\n";
} else {
echo "⚠️ ANTHROPIC_API_KEY: Not set in PHP environment\n";
echo " (This is expected - Python loads it from .env)\n";
}
$google_key = getenv('GOOGLE_API_KEY');
if ($google_key) {
echo "✅ GOOGLE_API_KEY: " . substr($google_key, 0, 20) . "..." . substr($google_key, -10) . "\n";
} else {
echo "⚠️ GOOGLE_API_KEY: Not set in PHP environment\n";
echo " (This is expected - Python loads it from .env)\n";
}
echo "\n==================================================\n";
echo "Summary\n";
echo "==================================================\n\n";
echo "✅ PHP backend is correctly configured\n";
echo " - .env file exists and will be loaded by Python\n";
echo " - PHP passes environment to Python subprocess\n";
echo " - Python's dotenv library loads .env automatically\n";
echo "\n";

View file

@ -1,82 +0,0 @@
#!/bin/bash
# Quick test script to diagnose issues
echo "================================"
echo "PDF Checker Quick Test"
echo "================================"
echo ""
# Check if sample PDF exists
if [ ! -f "sample_good.pdf" ]; then
echo "❌ sample_good.pdf not found"
echo "Creating a simple test PDF..."
python3 create_sample_pdfs.py 2>/dev/null || echo "⚠️ Could not create sample PDF"
fi
echo "1. Testing Python installation..."
if command -v python3 &> /dev/null; then
echo "✅ python3 found: $(python3 --version)"
else
echo "❌ python3 not found"
exit 1
fi
echo ""
echo "2. Testing venv..."
if [ -d "venv" ]; then
echo "✅ venv directory exists"
if [ -f "venv/bin/python3" ]; then
echo "✅ venv python: $(venv/bin/python3 --version)"
else
echo "❌ venv/bin/python3 not found"
echo "Run: python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt"
exit 1
fi
else
echo "❌ venv directory not found"
echo "Run: python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt"
exit 1
fi
echo ""
echo "3. Testing required packages..."
venv/bin/python3 -c "import pypdf, pdfplumber, PIL, numpy" 2>/dev/null
if [ $? -eq 0 ]; then
echo "✅ Core packages installed"
else
echo "❌ Missing packages. Run: source venv/bin/activate && pip install -r requirements.txt"
exit 1
fi
echo ""
echo "4. Testing python-dotenv..."
venv/bin/python3 -c "from dotenv import load_dotenv" 2>/dev/null
if [ $? -eq 0 ]; then
echo "✅ python-dotenv installed"
else
echo "⚠️ python-dotenv not installed (optional, but recommended)"
echo " Run: source venv/bin/activate && pip install python-dotenv"
fi
echo ""
echo "5. Running quick mode test on sample_good.pdf..."
echo " Command: venv/bin/python3 enterprise_pdf_checker.py sample_good.pdf --quick"
echo ""
timeout 30 venv/bin/python3 enterprise_pdf_checker.py sample_good.pdf --quick
if [ $? -eq 0 ]; then
echo ""
echo "✅ TEST PASSED - Quick mode works!"
else
echo ""
echo "❌ TEST FAILED - Check errors above"
echo ""
echo "Common issues:"
echo " - Missing python packages: pip install -r requirements.txt"
echo " - PDF file corrupted: try a different PDF"
echo " - Python version too old: need Python 3.8+"
fi
echo ""
echo "================================"

View file

@ -1,182 +0,0 @@
%PDF-1.3
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 12 0 R /F4 13 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 100 /Length 1451 /Subtype /Image
/Type /XObject /Width 300
>>
stream
Gb"0U:P__`(r5Yt,l\28,"<@I,_]>K;\UNM/2/TUKS@F<@6n$%)pH;'AY[?(A8K7P(<BUV5oP5_)H'2LKKEZjcgQ:2kA?a"F7-S[hR>5Ke+`K+F5HMYqn-F1kFQb_3ELetzzzzzzzzzzzzz!!!!-Pbh%7gc9ZY>2%[UT9kiZ\T1,>XXXaK2c%;%cCBBH/t-eFe<mAeVosi+]%+I7b*DM-b`:YfI!O-84F*Y%-VsFQr:*-H0'CR\NW8W"5/tgK[8jA:QSiOco;5659H$*"/mTr1!2ad+N;+?^WWt1`eAs^qgsZF`4ZI;I]RR,S3]^`m]A%l1@!&BKo1FT#))=VJh:"U\T0IATJlGalfWd2RW6Ce_a,eF4hn&('/ZG\QnX_n22I0.l#L2PGG<4U6.I5S59uF:B2f.^DIqI>c]6>'O%iZi'(<GmtFrDl3\>MUc*'J&[3496qX)6O+hJ>\EHSB<JTL]SgR3HS,l<m\[mZfno!UmjdH)pc9;$5$6\8r!fbf#@dhQA_Tlr_Z&X&h0OWQD=>oUnR_+Kbqs:Y#oqn6ih=9Bg/T8Il`+05Eg?K6mr9bhg$:!;X9d+($j:okI^Hj2U>`:CfL^$[VL(Ue1.BQ#4<Rg\UPXQ;8$GmkEm2k7l")qKa`D]6@3?imWMil%5KiR*/'BZX)CpX08^'-9Z%F$9s$kJ=7DN'Zc[2J9'pSMHtUUcll[Oj2-N@ie@@,_1NH*d+s=#Q[\59#nusFao2+Jp!"En4k`%&1?Qar/V&HY;s`MmK+@.?)-^<loLn*-f!>Sp(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=+(<L'``oUnR_BZB71_gVFKi6ImiV_R*m(n*\H:1NZppCt]9RMmc.[^O&n_kOSWeX6&R))Y#fI<s6`>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<Z$YljiKsZHzzzzzzzzzzzz!!!#uU&P*!Ym<5~>endstream
endobj
5 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 120 /Length 2263 /Subtype /Image
/Type /XObject /Width 350
>>
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[M<g<Xbzzzzzzzzzzzzzzzzzz!3#KQhP/$PWtnE/7YZ6JmdqSsNGZBuaYr[hoH+CbMs8jW"W_Z0qN&SO+8_>V^><!?7L9aGiB:36<aR>/De,dTs"dW/n=tYn@@IYt^3f"@Ih/A?Y]VGp81uG[peeoHYgio'hm`&MIoP`;r/k<j=`#c!V-O^Ah.5(#,1Rr/okLDu0@G?8`o9S$Z!k)PUpXA^;>knsZL?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<HY?0Cq*I\i-jtW=4!0<ul@qh'Vf;'o*UWk`#1N)&[24oLL'fr&5@hr!lI,3R.cr=ii;RD>%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'k<dXSlt<"1W_<I2JoD8(9!itST`nkfe9f",8"sjPfIeGqIZ).HHFI^4l7Z-bq:MF\'+;h^K:A?Y0%RGA!ZN0H'&mOF#RMlMRf5OBQKsqA8oC:T4JFJ5(U)27A*a+Q/ZA_BDIQ&4,qDk?+[RaV&PI03DW@\OR8<B=1>ThlUJEQ1tSl<L?kb#Y+AjVV5&0[cZ[T4PPh_O]$nPU1S7e3SVV8k+5QqcXkqauS+#Wco@:ELU60\bTJ9,$8o/&E)4WD(B,O(+b$[5f5d<X*`QRPT/R5dJT5Se:n!sojh$8)QNI0eI:JGEae'U77S-[>M_cum"<&&$L_map'IJT$]MO\$'cR$?=G<FPis.2APU&&r\4lsTu4hJ@mY1\XP1NiT9?8WTH?4:[?nPk84_$RGqQ8)':)=1uJ-rrm8GZd2@\#Z"R'U\9C]rq&Ph-E$N.RR(,cTjYaoUcUG<pssK?:sWC@_9$cVZ12PG:*,3HTcci]OrP&hVFiKP\XmAf=pn4`uUbo?p:ZM-3kl%5o6S!/7W?LMPi4_%o/MRZ]&>@b$[Gl5d<X*`QRNc#Iq?FVq,SV>5M?TNRN7Z)Ht[4f51-X?2?jF-N;'7m:-%G"'$G=S)fXD\;g6SI<pT2ogE`/c1M>%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`\;+<Yb+@W=<WrEUG@df\S%-@b,G.>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++Y1kCljHohQdV<decZl<<?`@a5PXaVK;YH"*gQ4lfN4]a(*GnWI7"=ACo_4aDD8X0,koFA(5olHOZul@-67O"73d-sO0a*q*@eg?50u-t-TK4%a=##T9-db@_\[hoL$lKB4Wc`<rSD)jN__D:qm6[UirR4')Bq-$kbJd'<h;54OeC'Qf2uA^4PDbRLnl0.?"\S[4j,k1;JAnh>6O0JW2?-+5R^$r32OZ](SrA7C$/D)7*C.tX"bNQSJCZ;,PaW7K48VY08N^RL6(qH1#:[Zn7US:L06WbDRKs)OL"1.Y3O2_eCKeaM2O-2O^p3(MRHGp$`VC&G)<?dm./dJm6TR>8MOe2W2sU\IlE0Yn(%I$QMZNK!=U<$e)(ckSi0<F$KjIO"pY%OqR2=B#J)Z)A'2@Sn!Czzzzzzzzzzzzzzzzzz!%ICZ[=\bf~>endstream
endobj
6 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 90 /Length 2549 /Subtype /Image
/Type /XObject /Width 280
>>
stream
Gb"0U$#g>t*!btg,d%GnKncJs5U@_PXUpaH)Ti3CWhW1eN^;K$ALJRAheM.!lABp.UPPpALo-1h8DKGcOG&E.+qjGBSbsfr41jtKHS9[,2<I!lREY+!s53kE^ANGls8Tf]-Bm+N6psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF2ru7_'0//kii9d)4WUf\/P`t-fWn>rHrJ#asCm5A2"&B_B^UJ.5Pg)(W4tUjAf'D)"GAH+82g'Isrrd%Tku'ZgpDf*>*^&'j%Alo!_-k#Hm)R^:BuZ,#j5QM<A5pRB?GHJOA7TAgI_V1!pVc1n8h.3@TNI-"W&JJ@6Amu`DZ$t#kgF%?VQ+_#>uHrS=0cl.$r(S`p^gCfHs!XaaZN9thnJDf_ha+TerJNh*iU_n0Nr1o`'5C=/bZ0)s,@upTEO@Flpm!P1EX/;nPE.^HpU/o>TODT3(;.<AANm'Pr(cWQ7j>]Cu2M]Akd,/Jj7EPmL@Y>H0!&eZ;jq+fa8Jn[CBSc,Q1K).J#A=+<K?2&$9%XQ?";NF*$0!][a$YlhbcPNu[EiE#XrL%j,\KHR19qji]m^o1L&^DXQ>m2,O;58\$0Bi`mN;<!\XWL^/Pj&f'!g#kmWLL^#5&I\8.)EMGG7e'bo!GMTh`e5]g]R4hm25WLIER]Yl)q$0n*Wq>puBJ<i00,AbH/WW<adb2aa[Er=#MEt.7`;buHhl+`kB52'#3Rgi,fO!6Gb*6W:p;e\nWouZ7.MeP;7l!NMoiXH!Y@%;R$BYq<LG-V5C23DS-!i"-*BNPN\AGIHe(_6D;c(2B;t$PULLVJg\u!B:)Wq]KhV8bR%NK.0X%N<epnT%O0[spgk`!J:[53m1mft4hnR?p2@+JrWBU^pY9=i)obG0Y/jchl*VF[gmrLjq"4\F_o")tM6Y\@!Ik0+,[aisD*9TB[)2fHE]Wcmb>":)t<-J#>J6bcQhH*h^0%lD(/=]OH'\&."82dmjZ.`C>7g6kJ)pX?"an$5N;#3QFZB?@PQPGYrS.`bI^aWkASU`Qna<jQG"a"iB"=IqMB-`-OhYneb@]t9K*.g\5[(J9s=Ngr^6o#9nTaZo'7C7Ie]-/H-')B+PS\O]?BnW24fQs_Ihn%MMGVY928Sc-Vuj7;<C0)p.E9B)0u1)3KF%NYC6<Y<>S=_3k4rq,H=Y^H*,7oG8e96PJmMg]%oL[t94a2mP93T"<=b*@2CHaK)/<N=hE11FUrTr7&u.G)Lf@,PbSl#?+/Tk_m&TffWY+,heV\n0&t0)p.E9B*$8Ot"hS8"R5O@'sk+KCT!L.>-0:/YckY<O(ONXL;e^9L;T4ZTtX'?U-lhPUIcrB$L>)m*Xs:n(?88?f*-*]dE_ec'g:C2nME;OZiZ53qY[;QRs0Anp`U3,gOOW-/dn,mD=RPe8p"]pDftG9"K3%J^k&?An!bFUU'a<!t6%[Nq.i+Is'_H9D)*u*^2uu8"4dWad7=`V2tZAePgHeNus^l=nB)u9HDA&S,Jj=pE!?O0-6fIKcN7dl44isjmo>m7l`)\PUY%:&W9?e;eG^SPk'ORW`<D9H/H=G=PgHdZ_eD(7ZAKS>@!%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[4Zo<A^&#/mDCflN)>CIdI:%'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(5G<F9M[`.*CDkL'=c]/>TjDdJYj1?`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
7 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 612 792 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject <<
/FormXob.2c2d8c1a59ccd390014a13df1823520c 6 0 R /FormXob.4239313bbffe37482d3f1e78247febb9 5 0 R /FormXob.c61c5faae8c5519bf83811c2a31afbe3 4 0 R
>>
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 80 /Length 1760 /Subtype /Image
/Type /XObject /Width 200
>>
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\<JO;dfCQ.`%,EoCmegVsbP!=Mc`G;((Yn>1Qa2([\Q]!WE`n%$X:JH`.Hf-pkQ$@Cla,]7W#ls#_nR4E*JhDk=_^$67ImA%Q*jsPZo%EU?hs^V7pj<NOZm%5MqJmoO$9RiKHYuq0^nElfkHXT8XFKN@qaXQN\E!LHUiC_3i]FET&;g.W3)1d1"=S+n8[A2F(L-F.Ku$R@fOE28"Clp73qTFm?*sJc':DFl[;iG4m"I]K!Bq3f]8gG*#nAs!#$8lAV\2u`,r9LgJs[G=T"i-1Y#FtfJZfU2%ZNuK@_U=Z)W#)El!dM?glq?TK9+N;`TTf@bnVM]9k*1KK,C>9XrAn9mOn#o+Z#1X./oD1%_XGSa;L)/*tl3eRO)Igg9(c=9P?3YHHNu1Rbk[:LU).nsp'X5g\g>O2i<mVD"M-f'OEjhf'h/L='PMCjGBF@rb,kA,kDdHcdEV>l4>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<n3*T(@Bbp!a[\kh\W#4jP^]uA?P8t`MX&JAE@;l74aT@%?7Y`]]054#AViMGrk_G&-\u[:5PQVF*/]"KNMoEYHOs23I!XLqt4X67(KB->\P6<pDA62SVg;,b!)ZRVW/jbXa+Z`5^](ir+(k53+>mk=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@<j1We3^?ZF3-&BU=n4cuU?P0!Kd$Da)b+lm+LBY?:9-:&c-V%N6,k-'$EUek'.jVDDMll(JBA!m1,NZ*C1$\;]6WGci0oq1+f-(*<a=d$f,_qa;]7ici[hN&JCi0,fGdOF[=V80<i-g/g^!U@QQ[)>/4RI=sXK:J,?`0/>^^Hh!HrBo2g!<pV1X'$oWLb!8)6J=h,Nb+co-e3#]Er%1Zd<Wajrp*Z:8XS0f'r#nmfshA0H0GN$@3`R*9"!![$E49K?ZR(%k8[2`O]d7.m"8+4=iPTl[ZcU&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J<DAoagkMd>.~>endstream
endobj
9 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 90 /Length 2270 /Subtype /Image
/Type /XObject /Width 250
>>
stream
Gb"0TI8!XP*!bu?=)2B:rFIL[<U7o;C2'm*S(3g?[8s>/XdQTi]!gmb[^Idi+ta!1:qXS4:d@8L'MpPrJg`9]G_&*oj[B;t]5lk:?7t$ILI[@8kAi3\CIb/gh.Q)Ek</Lok<AWechX,Q0jS?G&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J=7Cogk<ooF^UY`+:%758<isS9sUbYU%%h\*1l-06V]]CH%]2VJFo/2O&LQFs,_YQ':p/VdeHX*kT2EIG&9.^M`nEI:RR*Is]prrFYn#fWG+UG,:[DKPCJ$R=8l=\+R45gTY'gD"6j*nR)JXFWU"Y\(ImSZt%<DqOH2dJnb6d9I]?p[fiB5^p.n#3Fk?iYM<;q?=9A/kN:ATf4<C`)JnVO4)M%AWCpE`A=@MC6e:fZ]P)Raq5RY#Psgr1[eA;6etG7qa#p^FrDr!/Sc@Q=pU4/4]D>gc>*2Vn1,m/Gn3kt8li['\Hn5te(OPl/pWGa:.L7N>_;@'9@<[JIfm!Y;Fq#iQ>*W-"?9%?^H5lQWk=<Lu)bGP5ObE.$h3ueCl2fsEdh>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#<J:\\i(1$E%%S1a)KC;H17f>'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!q9<I-_q%%:)GIc*FJ_4uX).EJF?3I[[T[Q%?<<*Z`kk3Q-1/B&q1trKafIf*XtP!U<=p/r^rsb%=JQsubg/'+G&+AuNQ3q!o.`f/Sd^nm<Yi28.jDM+Nm_T'*-N1B#ah.UZKO'F2A\=L>s7W0<kQ7iQ03N*>Q=V70^V?XR<HLKI8KQNg;E@e([#RuX?N6D2q.4hko':A'<sh`Th1SA]4V_=o5+uk:]>gka/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<U(MaWMMk^R($_>]3UQ)WXLrhTkAL1Nqp](e_I6for/>(<NMIrhW+k(O4lk$Jjm<a%SE6l$kPB!(2UAaW(-Ef.<N/uep?`qNBl?2jm,PcmdOm/<;::9Nm&u[`!u6_rQ.)>/,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%$.>k<VW/CgHi*j`d/TtPibJgBfD4#X&RhfV%+)MF!"3kM]_@G]qhc<gT0(g:A`ZqOaL)Vk-@Z3<$YGAS)gs:&lK-"rl'-,5*M9ZU"qTa'"@_N%9r#nG[fBdUbhC,+\E4!Ehl-FX!ID,=L8V2%,a`PBpiBBpXPO9:8Mi4hq9Jc`Se+-(0e#sAo0W$I2iVDDl'D%1j:).pF::q\nUk@]<`:?.)UEC>OVK7@+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
10 0 obj
<<
/BitsPerComponent 8 /ColorSpace /DeviceRGB /Filter [ /ASCII85Decode /FlateDecode ] /Height 100 /Length 2098 /Subtype /Image
/Type /XObject /Width 320
>>
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%;FUMXjbi<oRt;6`JteWSh_ldoLWH<$lM\@AK?:tJ-25,kSGRj7rrniJfjq!-D18.[*q-eGJ)B@ZY+s7"u7feGBC[VF?m6DaTp[#C',>Ibd15JIF6*n6:0m/6bTml(+Ao=Jqu5.,DqJjNOUDtnEFXN^LjQ06>W09KcCq!g^!*7RRFC<u%`^SLc[/^r#T%1QL$G'.rlO/m:1$*0q[7[rl(^Mdt+eJ+c/HtR*Tm-Le\:RjCQF^it-2Q3uAA`r-rP8a-qm[7Dk4ABZBH2-l;;cAkmeDB&"j&7=hEnj37orIoaH'LL:n6j:s*Vp%G[r/m!j+]EREo]bk^#@WfY&*parp<m_&)P^]U!5/n[TpRrh0,X\>C'@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<m`B7D@%0_nmF`KTQ]tk5<(:WV:t07,2KdoQ9r0:Mk:-5Wi/%TW-bjD*PGUF:fs:G+Z"$AG%Hf\&O4=ciIC-E4FR7m(SfQh5Q=!p@<-%6OV:_!`KPOM';HJ3'8,agr2uH+"3I^n9b$Vo4D4A5P]f*%M^4!%$`go28\iW^0n&q%N,F*]JX+d^g6dOeIo@r'UC_c7#lJ1O1kd;_DB5`$<Lb$C>=j()&k#J/QGIOk`QgCc'fWOpdNr2Pmn*&tKUo',R?+OlO)&X>2$;V3h1GbJJ>$>*]-7Sb5T31pM=$t7[Lm2h2P)^L,n,E:_p%,Y2hdW)09PRM=B5`$<LatiAIAUhmLQ1\9s5qD;V#7eaE^sqSq)Qa>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*<e?nnj_SUo<oTdqVT$<CIRHNJLaiX,E(HX3#/]s!kVad>=[.(Q4p/j6N<&A;c=TmgJc't011du.7e]Q@ted1j$dEuCQH@("(<ZP7#ZX:Ir%>QS0\<6^Wfs<'91?d*Yrl3mSTZ+%D\[e`snF)HpD#)TdT:;<KiQ0)2;cAmnJ8t;L=`p&<042G`hUS4BOaiejWtfI?'hqf8=L9HR`g4$kfe@,:(&iV1,#$I*GB\9,kP$@MT0Mcc=3Kri)`OW6f6r-(GW-j@i=3Q%iLaK'%Z/:)rhSi6Pq><=OC/NZda^P+Oajdos0fAEWg`(adP<,I^V;uQ,2'A>fD,-N%IAuJeO7d5e"ckTDd&(U]mEh-;jtkSs"A%krSkeSq>#<dS=#\REofpSPlpcjZ2_S3@B9X=-6qnjH?ra2&2aktW9XB6VaDYCq>Z/g`^Y*/:0Yce<C4%)h>RXW]%X&Bzzzzzzzzzzzzz!!!#WYOE("02E8~>endstream
endobj
11 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 612 792 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] /XObject <<
/FormXob.1310210de56a359f75cadd6058093d5c 8 0 R /FormXob.85598c76e5387c61e079109a4090d1fe 10 0 R /FormXob.fe6121c1aa08a49ce6c0bd2422036546 9 0 R
>>
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
13 0 obj
<<
/BaseFont /Symbol /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
14 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 612 792 ] /Parent 17 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/PageMode /UseNone /Pages 17 0 R /Type /Catalog
>>
endobj
16 0 obj
<<
/Author (anonymous) /CreationDate (D:20251020161349-04'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20251020161349-04'00') /Producer (ReportLab PDF Library - www.reportlab.com)
/Subject (unspecified) /Title (untitled) /Trapped /False
>>
endobj
17 0 obj
<<
/Count 3 /Kids [ 7 0 R 11 0 R 14 0 R ] /Type /Pages
>>
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 341
>>
stream
GarWr9i&Y\$jPX:ItbE6&maiL1uX6udNf;FjhN`n',IsXJs<Hg:Y-'n#Xrd8=7TiGM"0G'\HB?`YZN(lJP1Nn<o@lRg/V'H5\cXLWQe5!HU8*Re2Z'rnZ@:sJ/>HT`hpOU*nK9/qZ*Zp?=GnqpB^3Zg\lWZTo68Cf!.WaZc`5in9GDZ%R(!@*)"BsDt<AuYIWQc+ns`3FKk/3P![CZplDX#&*C#u/GnVu^(3)n,O=E=1orRgOGl#P9O=Gh+\K90X1KCIpC'cT[(dJIdRo`IU_IC8%(.j!C^d9i`=VAP6Y9rsUsP`DLoE7j?<cPm=s6^fP\i`S;Np$AJa*p4#]m6~>endstream
endobj
19 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
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`nK2<aBfXW76cW@Xn6.)UI3TAg)YU-,:S@1@Y@,oZp1Ih%l$8;+t<Qm9SWZt1Rmdq!uZh:C#@kaEJQ#g*-FO3u80@>oG>q4iWhFc1hYI4r'_j8bX;T\rNki)>`]lI15^[ObkfsST8VodBK%7U*+4ust^O'%Jk&hHsIW1DRX-QC5H*H?@\rGCjBpH>n<pFV"SO'[^q#?LST4n2!.,#"X2_L!\h,(tfsFPG7;rAVi!7GdY`jEnI,#ZXm%9V`O4h'ntl%(?h6^"W)t.%GYckaT]4~>endstream
endobj
xref
0 21
0000000000 65535 f
0000000073 00000 n
0000000136 00000 n
0000000243 00000 n
0000000355 00000 n
0000001997 00000 n
0000004451 00000 n
0000007190 00000 n
0000007544 00000 n
0000009494 00000 n
0000011954 00000 n
0000014244 00000 n
0000014600 00000 n
0000014684 00000 n
0000014762 00000 n
0000014958 00000 n
0000015028 00000 n
0000015325 00000 n
0000015399 00000 n
0000015831 00000 n
0000016266 00000 n
trailer
<<
/ID
[<e9790a42050f762a07099aba1d88bb8b><e9790a42050f762a07099aba1d88bb8b>]
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
/Info 16 0 R
/Root 15 0 R
/Size 21
>>
startxref
16799
%%EOF

View file

@ -1,267 +0,0 @@
%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<Hg:Y-'n#Xrd8=7TiGM"0G'\HB?`YZN(lJP1Nn<o@lRg/V'H5\cXLWQe5!HU8*Re2Z'rnZ@:sJ/>HT`hpOU*nK9/qZ*Zp?=GnqpB^3Zg\lWZTo68Cf!.WaZc`5in9GDZ%R(!@*)"BsDt<AuYIWQc+ns`3FKk/3P![CZplDX#&*C#u/GnVu^(3)n,O=E=1orRgOGl#P9O=Gh+\K90X1KCIpC'cT[(dJIdRo`IU_IC8%(.j!C^d9i`=VAP6Y9rsUsP`DLoE7j?<cPm=s6^fP\i`S;Np$AJa*p4#]m6~>
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[,2<I!lREY+!s53kE^ANGls8Tf]-Bm+N6psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF26psF2ru7_'0//kii9d)4WUf\/P`t-fWn>rHrJ#asCm5A2"&B_B^UJ.5Pg)(W4tUjAf'D)"GAH+82g'Isrrd%Tku'ZgpDf*>*^&'j%Alo!_-k#Hm)R^:BuZ,#j5QM<A5pRB?GHJOA7TAgI_V1!pVc1n8h.3@TNI-"W&JJ@6Amu`DZ$t#kgF%?VQ+_#>uHrS=0cl.$r(S`p^gCfHs!XaaZN9thnJDf_ha+TerJNh*iU_n0Nr1o`'5C=/bZ0)s,@upTEO@Flpm!P1EX/;nPE.^HpU/o>TODT3(;.<AANm'Pr(cWQ7j>]Cu2M]Akd,/Jj7EPmL@Y>H0!&eZ;jq+fa8Jn[CBSc,Q1K).J#A=+<K?2&$9%XQ?";NF*$0!][a$YlhbcPNu[EiE#XrL%j,\KHR19qji]m^o1L&^DXQ>m2,O;58\$0Bi`mN;<!\XWL^/Pj&f'!g#kmWLL^#5&I\8.)EMGG7e'bo!GMTh`e5]g]R4hm25WLIER]Yl)q$0n*Wq>puBJ<i00,AbH/WW<adb2aa[Er=#MEt.7`;buHhl+`kB52'#3Rgi,fO!6Gb*6W:p;e\nWouZ7.MeP;7l!NMoiXH!Y@%;R$BYq<LG-V5C23DS-!i"-*BNPN\AGIHe(_6D;c(2B;t$PULLVJg\u!B:)Wq]KhV8bR%NK.0X%N<epnT%O0[spgk`!J:[53m1mft4hnR?p2@+JrWBU^pY9=i)obG0Y/jchl*VF[gmrLjq"4\F_o")tM6Y\@!Ik0+,[aisD*9TB[)2fHE]Wcmb>":)t<-J#>J6bcQhH*h^0%lD(/=]OH'\&."82dmjZ.`C>7g6kJ)pX?"an$5N;#3QFZB?@PQPGYrS.`bI^aWkASU`Qna<jQG"a"iB"=IqMB-`-OhYneb@]t9K*.g\5[(J9s=Ngr^6o#9nTaZo'7C7Ie]-/H-')B+PS\O]?BnW24fQs_Ihn%MMGVY928Sc-Vuj7;<C0)p.E9B)0u1)3KF%NYC6<Y<>S=_3k4rq,H=Y^H*,7oG8e96PJmMg]%oL[t94a2mP93T"<=b*@2CHaK)/<N=hE11FUrTr7&u.G)Lf@,PbSl#?+/Tk_m&TffWY+,heV\n0&t0)p.E9B*$8Ot"hS8"R5O@'sk+KCT!L.>-0:/YckY<O(ONXL;e^9L;T4ZTtX'?U-lhPUIcrB$L>)m*Xs:n(?88?f*-*]dE_ec'g:C2nME;OZiZ53qY[;QRs0Anp`U3,gOOW-/dn,mD=RPe8p"]pDftG9"K3%J^k&?An!bFUU'a<!t6%[Nq.i+Is'_H9D)*u*^2uu8"4dWad7=`V2tZAePgHeNus^l=nB)u9HDA&S,Jj=pE!?O0-6fIKcN7dl44isjmo>m7l`)\PUY%:&W9?e;eG^SPk'ORW`<D9H/H=G=PgHdZ_eD(7ZAKS>@!%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[4Zo<A^&#/mDCflN)>CIdI:%'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(5G<F9M[`.*CDkL'=c]/>TjDdJYj1?`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[M<g<Xbzzzzzzzzzzzzzzzzzz!3#KQhP/$PWtnE/7YZ6JmdqSsNGZBuaYr[hoH+CbMs8jW"W_Z0qN&SO+8_>V^><!?7L9aGiB:36<aR>/De,dTs"dW/n=tYn@@IYt^3f"@Ih/A?Y]VGp81uG[peeoHYgio'hm`&MIoP`;r/k<j=`#c!V-O^Ah.5(#,1Rr/okLDu0@G?8`o9S$Z!k)PUpXA^;>knsZL?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<HY?0Cq*I\i-jtW=4!0<ul@qh'Vf;'o*UWk`#1N)&[24oLL'fr&5@hr!lI,3R.cr=ii;RD>%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'k<dXSlt<"1W_<I2JoD8(9!itST`nkfe9f",8"sjPfIeGqIZ).HHFI^4l7Z-bq:MF\'+;h^K:A?Y0%RGA!ZN0H'&mOF#RMlMRf5OBQKsqA8oC:T4JFJ5(U)27A*a+Q/ZA_BDIQ&4,qDk?+[RaV&PI03DW@\OR8<B=1>ThlUJEQ1tSl<L?kb#Y+AjVV5&0[cZ[T4PPh_O]$nPU1S7e3SVV8k+5QqcXkqauS+#Wco@:ELU60\bTJ9,$8o/&E)4WD(B,O(+b$[5f5d<X*`QRPT/R5dJT5Se:n!sojh$8)QNI0eI:JGEae'U77S-[>M_cum"<&&$L_map'IJT$]MO\$'cR$?=G<FPis.2APU&&r\4lsTu4hJ@mY1\XP1NiT9?8WTH?4:[?nPk84_$RGqQ8)':)=1uJ-rrm8GZd2@\#Z"R'U\9C]rq&Ph-E$N.RR(,cTjYaoUcUG<pssK?:sWC@_9$cVZ12PG:*,3HTcci]OrP&hVFiKP\XmAf=pn4`uUbo?p:ZM-3kl%5o6S!/7W?LMPi4_%o/MRZ]&>@b$[Gl5d<X*`QRNc#Iq?FVq,SV>5M?TNRN7Z)Ht[4f51-X?2?jF-N;'7m:-%G"'$G=S)fXD\;g6SI<pT2ogE`/c1M>%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`\;+<Yb+@W=<WrEUG@df\S%-@b,G.>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++Y1kCljHohQdV<decZl<<?`@a5PXaVK;YH"*gQ4lfN4]a(*GnWI7"=ACo_4aDD8X0,koFA(5olHOZul@-67O"73d-sO0a*q*@eg?50u-t-TK4%a=##T9-db@_\[hoL$lKB4Wc`<rSD)jN__D:qm6[UirR4')Bq-$kbJd'<h;54OeC'Qf2uA^4PDbRLnl0.?"\S[4j,k1;JAnh>6O0JW2?-+5R^$r32OZ](SrA7C$/D)7*C.tX"bNQSJCZ;,PaW7K48VY08N^RL6(qH1#:[Zn7US:L06WbDRKs)OL"1.Y3O2_eCKeaM2O-2O^p3(MRHGp$`VC&G)<?dm./dJm6TR>8MOe2W2sU\IlE0Yn(%I$QMZNK!=U<$e)(ckSi0<F$KjIO"pY%OqR2=B#J)Z)A'2@Sn!Czzzzzzzzzzzzzzzzzz!%ICZ[=\bf~>
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(<BUV5oP5_)H'2LKKEZjcgQ:2kA?a"F7-S[hR>5Ke+`K+F5HMYqn-F1kFQb_3ELetzzzzzzzzzzzzz!!!!-Pbh%7gc9ZY>2%[UT9kiZ\T1,>XXXaK2c%;%cCBBH/t-eFe<mAeVosi+]%+I7b*DM-b`:YfI!O-84F*Y%-VsFQr:*-H0'CR\NW8W"5/tgK[8jA:QSiOco;5659H$*"/mTr1!2ad+N;+?^WWt1`eAs^qgsZF`4ZI;I]RR,S3]^`m]A%l1@!&BKo1FT#))=VJh:"U\T0IATJlGalfWd2RW6Ce_a,eF4hn&('/ZG\QnX_n22I0.l#L2PGG<4U6.I5S59uF:B2f.^DIqI>c]6>'O%iZi'(<GmtFrDl3\>MUc*'J&[3496qX)6O+hJ>\EHSB<JTL]SgR3HS,l<m\[mZfno!UmjdH)pc9;$5$6\8r!fbf#@dhQA_Tlr_Z&X&h0OWQD=>oUnR_+Kbqs:Y#oqn6ih=9Bg/T8Il`+05Eg?K6mr9bhg$:!;X9d+($j:okI^Hj2U>`:CfL^$[VL(Ue1.BQ#4<Rg\UPXQ;8$GmkEm2k7l")qKa`D]6@3?imWMil%5KiR*/'BZX)CpX08^'-9Z%F$9s$kJ=7DN'Zc[2J9'pSMHtUUcll[Oj2-N@ie@@,_1NH*d+s=#Q[\59#nusFao2+Jp!"En4k`%&1?Qar/V&HY;s`MmK+@.?)-^<loLn*-f!>Sp(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=+(<L'``oUnR_BZB71_gVFKi6ImiV_R*m(n*\H:1NZppCt]9RMmc.[^O&n_kOSWeX6&R))Y#fI<s6`>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<Z$YljiKsZHzzzzzzzzzzzz!!!#uU&P*!Ym<5~>
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\<JO;dfCQ.`%,EoCmegVsbP!=Mc`G;((Yn>1Qa2([\Q]!WE`n%$X:JH`.Hf-pkQ$@Cla,]7W#ls#_nR4E*JhDk=_^$67ImA%Q*jsPZo%EU?hs^V7pj<NOZm%5MqJmoO$9RiKHYuq0^nElfkHXT8XFKN@qaXQN\E!LHUiC_3i]FET&;g.W3)1d1"=S+n8[A2F(L-F.Ku$R@fOE28"Clp73qTFm?*sJc':DFl[;iG4m"I]K!Bq3f]8gG*#nAs!#$8lAV\2u`,r9LgJs[G=T"i-1Y#FtfJZfU2%ZNuK@_U=Z)W#)El!dM?glq?TK9+N;`TTf@bnVM]9k*1KK,C>9XrAn9mOn#o+Z#1X./oD1%_XGSa;L)/*tl3eRO)Igg9(c=9P?3YHHNu1Rbk[:LU).nsp'X5g\g>O2i<mVD"M-f'OEjhf'h/L='PMCjGBF@rb,kA,kDdHcdEV>l4>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<n3*T(@Bbp!a[\kh\W#4jP^]uA?P8t`MX&JAE@;l74aT@%?7Y`]]054#AViMGrk_G&-\u[:5PQVF*/]"KNMoEYHOs23I!XLqt4X67(KB->\P6<pDA62SVg;,b!)ZRVW/jbXa+Z`5^](ir+(k53+>mk=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@<j1We3^?ZF3-&BU=n4cuU?P0!Kd$Da)b+lm+LBY?:9-:&c-V%N6,k-'$EUek'.jVDDMll(JBA!m1,NZ*C1$\;]6WGci0oq1+f-(*<a=d$f,_qa;]7ici[hN&JCi0,fGdOF[=V80<i-g/g^!U@QQ[)>/4RI=sXK:J,?`0/>^^Hh!HrBo2g!<pV1X'$oWLb!8)6J=h,Nb+co-e3#]Er%1Zd<Wajrp*Z:8XS0f'r#nmfshA0H0GN$@3`R*9"!![$E49K?ZR(%k8[2`O]d7.m"8+4=iPTl[ZcU&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J<DAoagkMd>.~>
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%;FUMXjbi<oRt;6`JteWSh_ldoLWH<$lM\@AK?:tJ-25,kSGRj7rrniJfjq!-D18.[*q-eGJ)B@ZY+s7"u7feGBC[VF?m6DaTp[#C',>Ibd15JIF6*n6:0m/6bTml(+Ao=Jqu5.,DqJjNOUDtnEFXN^LjQ06>W09KcCq!g^!*7RRFC<u%`^SLc[/^r#T%1QL$G'.rlO/m:1$*0q[7[rl(^Mdt+eJ+c/HtR*Tm-Le\:RjCQF^it-2Q3uAA`r-rP8a-qm[7Dk4ABZBH2-l;;cAkmeDB&"j&7=hEnj37orIoaH'LL:n6j:s*Vp%G[r/m!j+]EREo]bk^#@WfY&*parp<m_&)P^]U!5/n[TpRrh0,X\>C'@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<m`B7D@%0_nmF`KTQ]tk5<(:WV:t07,2KdoQ9r0:Mk:-5Wi/%TW-bjD*PGUF:fs:G+Z"$AG%Hf\&O4=ciIC-E4FR7m(SfQh5Q=!p@<-%6OV:_!`KPOM';HJ3'8,agr2uH+"3I^n9b$Vo4D4A5P]f*%M^4!%$`go28\iW^0n&q%N,F*]JX+d^g6dOeIo@r'UC_c7#lJ1O1kd;_DB5`$<Lb$C>=j()&k#J/QGIOk`QgCc'fWOpdNr2Pmn*&tKUo',R?+OlO)&X>2$;V3h1GbJJ>$>*]-7Sb5T31pM=$t7[Lm2h2P)^L,n,E:_p%,Y2hdW)09PRM=B5`$<LatiAIAUhmLQ1\9s5qD;V#7eaE^sqSq)Qa>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*<e?nnj_SUo<oTdqVT$<CIRHNJLaiX,E(HX3#/]s!kVad>=[.(Q4p/j6N<&A;c=TmgJc't011du.7e]Q@ted1j$dEuCQH@("(<ZP7#ZX:Ir%>QS0\<6^Wfs<'91?d*Yrl3mSTZ+%D\[e`snF)HpD#)TdT:;<KiQ0)2;cAmnJ8t;L=`p&<042G`hUS4BOaiejWtfI?'hqf8=L9HR`g4$kfe@,:(&iV1,#$I*GB\9,kP$@MT0Mcc=3Kri)`OW6f6r-(GW-j@i=3Q%iLaK'%Z/:)rhSi6Pq><=OC/NZda^P+Oajdos0fAEWg`(adP<,I^V;uQ,2'A>fD,-N%IAuJeO7d5e"ckTDd&(U]mEh-;jtkSs"A%krSkeSq>#<dS=#\REofpSPlpcjZ2_S3@B9X=-6qnjH?ra2&2aktW9XB6VaDYCq>Z/g`^Y*/:0Yce<C4%)h>RXW]%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[<U7o;C2'm*S(3g?[8s>/XdQTi]!gmb[^Idi+ta!1:qXS4:d@8L'MpPrJg`9]G_&*oj[B;t]5lk:?7t$ILI[@8kAi3\CIb/gh.Q)Ek</Lok<AWechX,Q0jS?G&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J5Te&J=7Cogk<ooF^UY`+:%758<isS9sUbYU%%h\*1l-06V]]CH%]2VJFo/2O&LQFs,_YQ':p/VdeHX*kT2EIG&9.^M`nEI:RR*Is]prrFYn#fWG+UG,:[DKPCJ$R=8l=\+R45gTY'gD"6j*nR)JXFWU"Y\(ImSZt%<DqOH2dJnb6d9I]?p[fiB5^p.n#3Fk?iYM<;q?=9A/kN:ATf4<C`)JnVO4)M%AWCpE`A=@MC6e:fZ]P)Raq5RY#Psgr1[eA;6etG7qa#p^FrDr!/Sc@Q=pU4/4]D>gc>*2Vn1,m/Gn3kt8li['\Hn5te(OPl/pWGa:.L7N>_;@'9@<[JIfm!Y;Fq#iQ>*W-"?9%?^H5lQWk=<Lu)bGP5ObE.$h3ueCl2fsEdh>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#<J:\\i(1$E%%S1a)KC;H17f>'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!q9<I-_q%%:)GIc*FJ_4uX).EJF?3I[[T[Q%?<<*Z`kk3Q-1/B&q1trKafIf*XtP!U<=p/r^rsb%=JQsubg/'+G&+AuNQ3q!o.`f/Sd^nm<Yi28.jDM+Nm_T'*-N1B#ah.UZKO'F2A\=L>s7W0<kQ7iQ03N*>Q=V70^V?XR<HLKI8KQNg;E@e([#RuX?N6D2q.4hko':A'<sh`Th1SA]4V_=o5+uk:]>gka/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<U(MaWMMk^R($_>]3UQ)WXLrhTkAL1Nqp](e_I6for/>(<NMIrhW+k(O4lk$Jjm<a%SE6l$kPB!(2UAaW(-Ef.<N/uep?`qNBl?2jm,PcmdOm/<;::9Nm&u[`!u6_rQ.)>/,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%$.>k<VW/CgHi*j`d/TtPibJgBfD4#X&RhfV%+)MF!"3kM]_@G]qhc<gT0(g:A`ZqOaL)Vk-@Z3<$YGAS)gs:&lK-"rl'-,5*M9ZU"qTa'"@_N%9r#nG[fBdUbhC,+\E4!Ehl-FX!ID,=L8V2%,a`PBpiBBpXPO9:8Mi4hq9Jc`Se+-(0e#sAo0W$I2iVDDl'D%1j:).pF::q\nUk@]<`:?.)UEC>OVK7@+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`nK2<aBfXW76cW@Xn6.)UI3TAg)YU-,:S@1@Y@,oZp1Ih%l$8;+t<Qm9SWZt1Rmdq!uZh:C#@kaEJQ#g*-FO3u80@>oG>q4iWhFc1hYI4r'_j8bX;T\rNki)>`]lI15^[ObkfsST8VodBK%7U*+4ust^O'%Jk&hHsIW1DRX-QC5H*H?@\rGCjBpH>n<pFV"SO'[^q#?LST4n2!.,#"X2_L!\h,(tfsFPG7;rAVi!7GdY`jEnI,#ZXm%9V`O4h'ntl%(?h6^"W)t.%GYckaT]4~>
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

View file

@ -128,7 +128,7 @@ class TestAPIEndpoints:
assert 'Access-Control-Allow-Origin' in response.headers
# CORS now returns specific origin or localhost in dev mode
origin = response.headers['Access-Control-Allow-Origin']
assert origin in ['*', 'https://ai-sandbox.oliver.solutions', 'http://localhost:8888', 'http://localhost:8000', 'null']
assert origin in ['*', 'http://localhost:8888', 'http://localhost:8000', 'null']
def test_api_handles_options(self, php_server):
"""Test that API handles OPTIONS preflight requests"""