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:
parent
cfa7eeeeac
commit
5a00ec88d7
33 changed files with 59 additions and 6831 deletions
|
|
@ -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
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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/ 🚀
|
||||
|
|
@ -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**
|
||||
|
|
@ -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 →
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -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! 🚀
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════
|
||||
|
|
@ -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 ""
|
||||
|
|
@ -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
793
README.md
|
|
@ -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.
|
||||
|
|
|
|||
198
auth.php
198
auth.php
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
|
@ -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'
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
226
deploy.sh
|
|
@ -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 ""
|
||||
|
|
@ -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;'
|
||||
71
history.html
71
history.html
|
|
@ -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;">⬆ 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;">↺ 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>
|
||||
266
index.html
266
index.html
|
|
@ -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;">📂 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">📄</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">
|
||||
▶ 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">📚</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 & 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)">📄 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 & 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;">⬆ 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>
|
||||
86
js/api.js
86
js/api.js
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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
154
js/app.js
|
|
@ -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();
|
||||
});
|
||||
42
nginx.conf
42
nginx.conf
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 · 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 {sc.get('error',0)} errors {sc.get('warning',0)} warnings {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 · Enterprise Edition · Oliver Solutions · {now}
|
||||
PDF Accessibility SaaS · Aimpress · {now}
|
||||
</footer>
|
||||
</body>
|
||||
</html>"""
|
||||
|
|
|
|||
|
|
@ -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! 🎯
|
||||
|
|
@ -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
|
||||
275
test_fixed.pdf
275
test_fixed.pdf
|
|
@ -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
|
||||
|
|
@ -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";
|
||||
|
|
@ -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 "================================"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue