feat(tests): add e2e tests for multiple ocr languages
This commit is contained in:
parent
9b1ad3a596
commit
dde54a5361
|
|
@ -0,0 +1,346 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create test PDFs with Spanish and English content for OCR multiple language testing.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
except ImportError:
|
||||
print("reportlab not installed. Please install it with: pip install reportlab")
|
||||
print("Creating simple text files as fallback...")
|
||||
|
||||
def create_simple_multilingual_files():
|
||||
"""Create simple text files as a fallback"""
|
||||
test_dir = "frontend/test_data/multilingual"
|
||||
os.makedirs(test_dir, exist_ok=True)
|
||||
|
||||
# Spanish content
|
||||
spanish_content = """Hola mundo, este es un documento en español.
|
||||
Este documento contiene texto en español para probar el reconocimiento óptico de caracteres.
|
||||
Las palabras incluyen acentos como café, niño, comunicación y corazón.
|
||||
También incluye números como 123, 456 y fechas como 15 de marzo de 2024.
|
||||
El sistema OCR debe reconocer correctamente este contenido en español."""
|
||||
|
||||
# English content
|
||||
english_content = """Hello world, this is an English document.
|
||||
This document contains English text for optical character recognition testing.
|
||||
The words include common English vocabulary and technical terms.
|
||||
It also includes numbers like 123, 456 and dates like March 15, 2024.
|
||||
The OCR system should correctly recognize this English content."""
|
||||
|
||||
# Mixed content
|
||||
mixed_content = """Documento bilingüe / Bilingual Document
|
||||
|
||||
Sección en español:
|
||||
Este es un documento que contiene texto en dos idiomas diferentes.
|
||||
El reconocimiento óptico de caracteres debe manejar ambos idiomas.
|
||||
|
||||
English section:
|
||||
This is a document that contains text in two different languages.
|
||||
The optical character recognition should handle both languages."""
|
||||
|
||||
with open(f"{test_dir}/spanish_test.txt", "w", encoding="utf-8") as f:
|
||||
f.write(spanish_content)
|
||||
|
||||
with open(f"{test_dir}/english_test.txt", "w", encoding="utf-8") as f:
|
||||
f.write(english_content)
|
||||
|
||||
with open(f"{test_dir}/mixed_language_test.txt", "w", encoding="utf-8") as f:
|
||||
f.write(mixed_content)
|
||||
|
||||
print("Created simple multilingual text files for testing")
|
||||
return True
|
||||
|
||||
if not create_simple_multilingual_files():
|
||||
exit(1)
|
||||
exit(0)
|
||||
|
||||
def create_multilingual_test_pdfs():
|
||||
"""Create test PDFs with Spanish and English content"""
|
||||
test_dir = "frontend/test_data/multilingual"
|
||||
os.makedirs(test_dir, exist_ok=True)
|
||||
|
||||
# Spanish test PDF
|
||||
pdf_path = f"{test_dir}/spanish_test.pdf"
|
||||
c = canvas.Canvas(pdf_path, pagesize=letter)
|
||||
width, height = letter
|
||||
|
||||
# Spanish content
|
||||
c.setFont("Helvetica", 14)
|
||||
y_position = height - 80
|
||||
|
||||
# Title
|
||||
c.drawString(72, y_position, "Documento de Prueba en Español")
|
||||
y_position -= 40
|
||||
|
||||
c.setFont("Helvetica", 12)
|
||||
spanish_lines = [
|
||||
"Hola mundo, este es un documento en español.",
|
||||
"",
|
||||
"Este documento contiene texto en español para probar",
|
||||
"el reconocimiento óptico de caracteres (OCR).",
|
||||
"",
|
||||
"Las palabras incluyen acentos como:",
|
||||
"• café, niño, comunicación, corazón",
|
||||
"• también, habitación, compañía",
|
||||
"• informática, educación, investigación",
|
||||
"",
|
||||
"Números y fechas en español:",
|
||||
"• 123 ciento veintitrés",
|
||||
"• 456 cuatrocientos cincuenta y seis",
|
||||
"• 15 de marzo de 2024",
|
||||
"• 31 de diciembre de 2023",
|
||||
"",
|
||||
"Frases comunes:",
|
||||
"Por favor, muchas gracias, de nada.",
|
||||
"¿Cómo está usted? Muy bien, gracias.",
|
||||
"Buenos días, buenas tardes, buenas noches.",
|
||||
"",
|
||||
"El sistema OCR debe reconocer correctamente",
|
||||
"todo este contenido en español, incluyendo",
|
||||
"los caracteres especiales y acentos.",
|
||||
]
|
||||
|
||||
for line in spanish_lines:
|
||||
if line:
|
||||
c.drawString(72, y_position, line)
|
||||
y_position -= 18
|
||||
if y_position < 50: # Start new page if needed
|
||||
c.showPage()
|
||||
y_position = height - 50
|
||||
|
||||
c.save()
|
||||
print(f"Created: {pdf_path}")
|
||||
|
||||
# English test PDF
|
||||
pdf_path = f"{test_dir}/english_test.pdf"
|
||||
c = canvas.Canvas(pdf_path, pagesize=letter)
|
||||
|
||||
c.setFont("Helvetica", 14)
|
||||
y_position = height - 80
|
||||
|
||||
# Title
|
||||
c.drawString(72, y_position, "English Test Document")
|
||||
y_position -= 40
|
||||
|
||||
c.setFont("Helvetica", 12)
|
||||
english_lines = [
|
||||
"Hello world, this is an English document.",
|
||||
"",
|
||||
"This document contains English text for testing",
|
||||
"optical character recognition (OCR) capabilities.",
|
||||
"",
|
||||
"Common English words and phrases:",
|
||||
"• technology, computer, software, hardware",
|
||||
"• document, recognition, character, optical",
|
||||
"• testing, validation, verification, quality",
|
||||
"",
|
||||
"Numbers and dates in English:",
|
||||
"• 123 one hundred twenty-three",
|
||||
"• 456 four hundred fifty-six",
|
||||
"• March 15, 2024",
|
||||
"• December 31, 2023",
|
||||
"",
|
||||
"Common phrases:",
|
||||
"Please, thank you, you're welcome.",
|
||||
"How are you? I'm fine, thank you.",
|
||||
"Good morning, good afternoon, good evening.",
|
||||
"",
|
||||
"The OCR system should correctly recognize",
|
||||
"all this English content, including proper",
|
||||
"capitalization and punctuation marks.",
|
||||
"",
|
||||
"Technical terms and abbreviations:",
|
||||
"API, REST, JSON, XML, HTTP, HTTPS",
|
||||
"CPU, RAM, SSD, USB, WiFi, Bluetooth",
|
||||
]
|
||||
|
||||
for line in english_lines:
|
||||
if line:
|
||||
c.drawString(72, y_position, line)
|
||||
y_position -= 18
|
||||
if y_position < 50:
|
||||
c.showPage()
|
||||
y_position = height - 50
|
||||
|
||||
c.save()
|
||||
print(f"Created: {pdf_path}")
|
||||
|
||||
# Mixed language PDF
|
||||
pdf_path = f"{test_dir}/mixed_language_test.pdf"
|
||||
c = canvas.Canvas(pdf_path, pagesize=letter)
|
||||
|
||||
c.setFont("Helvetica", 14)
|
||||
y_position = height - 80
|
||||
|
||||
# Title
|
||||
c.drawString(72, y_position, "Documento Bilingüe / Bilingual Document")
|
||||
y_position -= 40
|
||||
|
||||
c.setFont("Helvetica", 12)
|
||||
mixed_lines = [
|
||||
"Sección en español:",
|
||||
"",
|
||||
"Este es un documento que contiene texto en dos",
|
||||
"idiomas diferentes. El reconocimiento óptico",
|
||||
"de caracteres debe manejar ambos idiomas",
|
||||
"correctamente y sin confusión.",
|
||||
"",
|
||||
"Palabras clave: español, idioma, reconocimiento",
|
||||
"",
|
||||
"English section:",
|
||||
"",
|
||||
"This is a document that contains text in two",
|
||||
"different languages. The optical character",
|
||||
"recognition should handle both languages",
|
||||
"correctly without confusion.",
|
||||
"",
|
||||
"Keywords: English, language, recognition",
|
||||
"",
|
||||
"Conclusión / Conclusion:",
|
||||
"",
|
||||
"Los sistemas modernos de OCR deben ser capaces",
|
||||
"de procesar múltiples idiomas en un solo documento.",
|
||||
"",
|
||||
"Modern OCR systems should be capable of processing",
|
||||
"multiple languages within a single document.",
|
||||
]
|
||||
|
||||
for line in mixed_lines:
|
||||
if line:
|
||||
c.drawString(72, y_position, line)
|
||||
y_position -= 18
|
||||
if y_position < 50:
|
||||
c.showPage()
|
||||
y_position = height - 50
|
||||
|
||||
c.save()
|
||||
print(f"Created: {pdf_path}")
|
||||
|
||||
# Complex Spanish document with special characters
|
||||
pdf_path = f"{test_dir}/spanish_complex.pdf"
|
||||
c = canvas.Canvas(pdf_path, pagesize=letter)
|
||||
|
||||
c.setFont("Helvetica", 14)
|
||||
y_position = height - 80
|
||||
|
||||
c.drawString(72, y_position, "Documento Español Complejo")
|
||||
y_position -= 40
|
||||
|
||||
c.setFont("Helvetica", 12)
|
||||
complex_spanish_lines = [
|
||||
"Características especiales del español:",
|
||||
"",
|
||||
"Vocales acentuadas: á, é, í, ó, ú",
|
||||
"Letra eñe: niño, España, año, señor",
|
||||
"Diéresis: pingüino, cigüeña, vergüenza",
|
||||
"",
|
||||
"Signos de puntuación especiales:",
|
||||
"¿Preguntas con signos de apertura?",
|
||||
"¡Exclamaciones con signos de apertura!",
|
||||
"",
|
||||
"Palabras con combinaciones complejas:",
|
||||
"• excelente, exacto, oxígeno",
|
||||
"• desarrollo, rápido, árbol",
|
||||
"• comunicación, administración, información",
|
||||
"",
|
||||
"Números ordinales:",
|
||||
"1º primero, 2º segundo, 3º tercero",
|
||||
"10º décimo, 20º vigésimo, 100º centésimo",
|
||||
"",
|
||||
"Este documento prueba la capacidad del OCR",
|
||||
"para reconocer correctamente todos los",
|
||||
"caracteres especiales del idioma español.",
|
||||
]
|
||||
|
||||
for line in complex_spanish_lines:
|
||||
if line:
|
||||
c.drawString(72, y_position, line)
|
||||
y_position -= 18
|
||||
if y_position < 50:
|
||||
c.showPage()
|
||||
y_position = height - 50
|
||||
|
||||
c.save()
|
||||
print(f"Created: {pdf_path}")
|
||||
|
||||
# Complex English document
|
||||
pdf_path = f"{test_dir}/english_complex.pdf"
|
||||
c = canvas.Canvas(pdf_path, pagesize=letter)
|
||||
|
||||
c.setFont("Helvetica", 14)
|
||||
y_position = height - 80
|
||||
|
||||
c.drawString(72, y_position, "Complex English Document")
|
||||
y_position -= 40
|
||||
|
||||
c.setFont("Helvetica", 12)
|
||||
complex_english_lines = [
|
||||
"Advanced English language features:",
|
||||
"",
|
||||
"Contractions: don't, won't, can't, isn't",
|
||||
"Possessives: user's, system's, company's",
|
||||
"Hyphenated words: state-of-the-art, well-known",
|
||||
"",
|
||||
"Technical terminology:",
|
||||
"• machine learning, artificial intelligence",
|
||||
"• natural language processing, deep learning",
|
||||
"• computer vision, pattern recognition",
|
||||
"",
|
||||
"Abbreviations and acronyms:",
|
||||
"• CEO, CTO, API, SDK, IDE, URL",
|
||||
"• HTML, CSS, JavaScript, TypeScript",
|
||||
"• REST, GraphQL, JSON, XML, YAML",
|
||||
"",
|
||||
"Numbers and measurements:",
|
||||
"• 3.14159 (pi), 2.71828 (e)",
|
||||
"• 100%, 50°F, 25°C, $1,000.00",
|
||||
"• 1st, 2nd, 3rd, 21st century",
|
||||
"",
|
||||
"This document tests the OCR system's ability",
|
||||
"to recognize complex English text patterns",
|
||||
"including technical terms and formatting.",
|
||||
]
|
||||
|
||||
for line in complex_english_lines:
|
||||
if line:
|
||||
c.drawString(72, y_position, line)
|
||||
y_position -= 18
|
||||
if y_position < 50:
|
||||
c.showPage()
|
||||
y_position = height - 50
|
||||
|
||||
c.save()
|
||||
print(f"Created: {pdf_path}")
|
||||
|
||||
print("\n🌍 Multilingual Test Files Summary:")
|
||||
print("=" * 50)
|
||||
|
||||
# Check file sizes
|
||||
test_files = [
|
||||
"spanish_test.pdf",
|
||||
"english_test.pdf",
|
||||
"mixed_language_test.pdf",
|
||||
"spanish_complex.pdf",
|
||||
"english_complex.pdf"
|
||||
]
|
||||
|
||||
for filename in test_files:
|
||||
filepath = f"{test_dir}/{filename}"
|
||||
if os.path.exists(filepath):
|
||||
size_bytes = os.path.getsize(filepath)
|
||||
size_kb = size_bytes / 1024
|
||||
print(f"📄 {filename}: {size_kb:.1f} KB ({size_bytes:,} bytes)")
|
||||
|
||||
print(f"\n✅ All multilingual test PDFs created in: {test_dir}/")
|
||||
print("🔤 Languages: Spanish (spa) and English (eng)")
|
||||
print("📝 Ready for OCR multiple language testing!")
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_multilingual_test_pdfs()
|
||||
|
|
@ -0,0 +1,501 @@
|
|||
import { test, expect } from './fixtures/auth';
|
||||
import { TIMEOUTS, API_ENDPOINTS, TEST_FILES } from './utils/test-data';
|
||||
import { TestHelpers } from './utils/test-helpers';
|
||||
|
||||
// Test data for multilingual OCR testing
|
||||
const MULTILINGUAL_TEST_FILES = {
|
||||
spanish: TEST_FILES.spanishTest,
|
||||
english: TEST_FILES.englishTest,
|
||||
mixed: TEST_FILES.mixedLanguageTest,
|
||||
spanishComplex: TEST_FILES.spanishComplex,
|
||||
englishComplex: TEST_FILES.englishComplex
|
||||
};
|
||||
|
||||
const EXPECTED_CONTENT = {
|
||||
spanish: {
|
||||
keywords: ['español', 'documento', 'reconocimiento', 'café', 'niño', 'comunicación'],
|
||||
phrases: ['Hola mundo', 'este es un documento', 'en español']
|
||||
},
|
||||
english: {
|
||||
keywords: ['English', 'document', 'recognition', 'technology', 'computer'],
|
||||
phrases: ['Hello world', 'this is an English', 'document']
|
||||
},
|
||||
mixed: {
|
||||
spanish: ['español', 'idiomas', 'reconocimiento'],
|
||||
english: ['English', 'languages', 'recognition']
|
||||
}
|
||||
};
|
||||
|
||||
const OCR_LANGUAGES = {
|
||||
spanish: { code: 'spa', name: 'Spanish' },
|
||||
english: { code: 'eng', name: 'English' },
|
||||
auto: { code: 'auto', name: 'Auto-detect' }
|
||||
};
|
||||
|
||||
test.describe('OCR Multiple Languages', () => {
|
||||
let helpers: TestHelpers;
|
||||
|
||||
test.beforeEach(async ({ adminPage }) => {
|
||||
helpers = new TestHelpers(adminPage);
|
||||
await helpers.navigateToPage('/settings');
|
||||
});
|
||||
|
||||
test('should display OCR language selector in settings', async ({ adminPage: page }) => {
|
||||
// Navigate to settings page
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for OCR language selector component
|
||||
const languageSelector = page.locator('[data-testid="ocr-language-selector"], #ocr-language-label').first();
|
||||
await expect(languageSelector).toBeVisible({ timeout: TIMEOUTS.medium });
|
||||
|
||||
// Check if the selector shows available languages
|
||||
const selectInput = page.locator('div[role="combobox"], select[id*="ocr"], input[id*="language"]').first();
|
||||
if (await selectInput.isVisible()) {
|
||||
await selectInput.click();
|
||||
|
||||
// Wait for language options to appear
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Check for Spanish and English options
|
||||
const spanishOption = page.locator('[data-value="spa"], option[value="spa"], :has-text("Spanish")').first();
|
||||
const englishOption = page.locator('[data-value="eng"], option[value="eng"], :has-text("English")').first();
|
||||
|
||||
if (await spanishOption.isVisible({ timeout: 3000 })) {
|
||||
console.log('✅ Spanish language option found');
|
||||
}
|
||||
if (await englishOption.isVisible({ timeout: 3000 })) {
|
||||
console.log('✅ English language option found');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should change OCR language preference to Spanish', async ({ adminPage: page }) => {
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Find and interact with language selector
|
||||
const languageSelector = page.locator('[data-testid="ocr-language-selector"], div:has(label:text("OCR Language"))').first();
|
||||
|
||||
if (await languageSelector.isVisible()) {
|
||||
// Click on the selector to open dropdown
|
||||
await languageSelector.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Select Spanish option
|
||||
const spanishOption = page.locator('[data-value="spa"], option[value="spa"], li:has-text("Spanish")').first();
|
||||
if (await spanishOption.isVisible({ timeout: 5000 })) {
|
||||
await spanishOption.click();
|
||||
|
||||
// Look for save button or auto-save indication
|
||||
const saveButton = page.locator('button:has-text("Save"), button[type="submit"]').first();
|
||||
if (await saveButton.isVisible({ timeout: 3000 })) {
|
||||
// Wait for settings update API call
|
||||
const updatePromise = helpers.waitForApiCall('/api/settings', TIMEOUTS.medium);
|
||||
await saveButton.click();
|
||||
await updatePromise;
|
||||
}
|
||||
|
||||
// Check for success indication
|
||||
await helpers.waitForToast();
|
||||
console.log('✅ OCR language changed to Spanish');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should upload Spanish document and process with Spanish OCR', async ({ adminPage: page }) => {
|
||||
// First set language to Spanish
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
const languageSelector = page.locator('div:has(label:text("OCR Language")), [data-testid="ocr-language-selector"]').first();
|
||||
if (await languageSelector.isVisible()) {
|
||||
await languageSelector.click();
|
||||
const spanishOption = page.locator('[data-value="spa"], li:has-text("Spanish")').first();
|
||||
if (await spanishOption.isVisible({ timeout: 5000 })) {
|
||||
await spanishOption.click();
|
||||
|
||||
const saveButton = page.locator('button:has-text("Save")').first();
|
||||
if (await saveButton.isVisible()) {
|
||||
await saveButton.click();
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to upload page
|
||||
await page.goto('/upload');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Upload Spanish test document
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
await expect(fileInput).toBeAttached({ timeout: 10000 });
|
||||
|
||||
try {
|
||||
await fileInput.setInputFiles(MULTILINGUAL_TEST_FILES.spanish);
|
||||
|
||||
// Verify file appears in upload list
|
||||
await expect(page.getByText('spanish_test.pdf')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click upload button
|
||||
const uploadButton = page.locator('button:has-text("Upload")').first();
|
||||
if (await uploadButton.isVisible()) {
|
||||
// Wait for upload and OCR processing
|
||||
const uploadPromise = helpers.waitForApiCall('/api/documents', TIMEOUTS.upload);
|
||||
await uploadButton.click();
|
||||
await uploadPromise;
|
||||
|
||||
// Wait for OCR processing to complete
|
||||
await page.waitForTimeout(3000);
|
||||
console.log('✅ Spanish document uploaded and OCR initiated');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('ℹ️ Spanish test file not found, skipping upload test');
|
||||
}
|
||||
});
|
||||
|
||||
test('should upload English document and process with English OCR', async ({ adminPage: page }) => {
|
||||
// First set language to English
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
const languageSelector = page.locator('div:has(label:text("OCR Language")), [data-testid="ocr-language-selector"]').first();
|
||||
if (await languageSelector.isVisible()) {
|
||||
await languageSelector.click();
|
||||
const englishOption = page.locator('[data-value="eng"], li:has-text("English")').first();
|
||||
if (await englishOption.isVisible({ timeout: 5000 })) {
|
||||
await englishOption.click();
|
||||
|
||||
const saveButton = page.locator('button:has-text("Save")').first();
|
||||
if (await saveButton.isVisible()) {
|
||||
await saveButton.click();
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to upload page
|
||||
await page.goto('/upload');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Upload English test document
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
await expect(fileInput).toBeAttached({ timeout: 10000 });
|
||||
|
||||
try {
|
||||
await fileInput.setInputFiles(MULTILINGUAL_TEST_FILES.english);
|
||||
|
||||
// Verify file appears in upload list
|
||||
await expect(page.getByText('english_test.pdf')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Click upload button
|
||||
const uploadButton = page.locator('button:has-text("Upload")').first();
|
||||
if (await uploadButton.isVisible()) {
|
||||
// Wait for upload and OCR processing
|
||||
const uploadPromise = helpers.waitForApiCall('/api/documents', TIMEOUTS.upload);
|
||||
await uploadButton.click();
|
||||
await uploadPromise;
|
||||
|
||||
// Wait for OCR processing to complete
|
||||
await page.waitForTimeout(3000);
|
||||
console.log('✅ English document uploaded and OCR initiated');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('ℹ️ English test file not found, skipping upload test');
|
||||
}
|
||||
});
|
||||
|
||||
test('should validate OCR results contain expected language-specific content', async ({ adminPage: page }) => {
|
||||
await page.goto('/documents');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for uploaded documents
|
||||
const documentItems = page.locator('.document-item, .document-card, [data-testid="document-item"]');
|
||||
const documentCount = await documentItems.count();
|
||||
|
||||
if (documentCount > 0) {
|
||||
// Click on first document to view details
|
||||
await documentItems.first().click();
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for document content or OCR text
|
||||
const contentArea = page.locator('.document-content, .ocr-text, [data-testid="document-content"]').first();
|
||||
|
||||
if (await contentArea.isVisible({ timeout: TIMEOUTS.medium })) {
|
||||
const contentText = await contentArea.textContent();
|
||||
|
||||
if (contentText) {
|
||||
// Check for Spanish keywords
|
||||
const hasSpanishContent = EXPECTED_CONTENT.spanish.keywords.some(keyword =>
|
||||
contentText.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
// Check for English keywords
|
||||
const hasEnglishContent = EXPECTED_CONTENT.english.keywords.some(keyword =>
|
||||
contentText.toLowerCase().includes(keyword.toLowerCase())
|
||||
);
|
||||
|
||||
if (hasSpanishContent) {
|
||||
console.log('✅ Spanish OCR content detected');
|
||||
}
|
||||
if (hasEnglishContent) {
|
||||
console.log('✅ English OCR content detected');
|
||||
}
|
||||
|
||||
console.log(`📄 Document content preview: ${contentText.substring(0, 100)}...`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ No documents found for content validation');
|
||||
}
|
||||
});
|
||||
|
||||
test('should retry failed OCR with different language', async ({ adminPage: page }) => {
|
||||
await page.goto('/documents');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for failed documents or retry options
|
||||
const retryButton = page.locator('button:has-text("Retry"), [data-testid="retry-ocr"]').first();
|
||||
|
||||
if (await retryButton.isVisible()) {
|
||||
// Look for language selection in retry dialog
|
||||
await retryButton.click();
|
||||
|
||||
// Check if retry dialog opens with language options
|
||||
const retryDialog = page.locator('.retry-dialog, [role="dialog"], .modal').first();
|
||||
if (await retryDialog.isVisible({ timeout: 5000 })) {
|
||||
|
||||
// Look for language selector in retry dialog
|
||||
const retryLanguageSelector = page.locator('select, [role="combobox"]').first();
|
||||
if (await retryLanguageSelector.isVisible()) {
|
||||
// Change language for retry
|
||||
await retryLanguageSelector.click();
|
||||
|
||||
const spanishRetryOption = page.locator('[data-value="spa"], option[value="spa"]').first();
|
||||
if (await spanishRetryOption.isVisible()) {
|
||||
await spanishRetryOption.click();
|
||||
|
||||
// Confirm retry with new language
|
||||
const confirmRetryButton = page.locator('button:has-text("Retry"), button:has-text("Confirm")').last();
|
||||
if (await confirmRetryButton.isVisible()) {
|
||||
const retryPromise = helpers.waitForApiCall('/retry', TIMEOUTS.ocr);
|
||||
await confirmRetryButton.click();
|
||||
|
||||
try {
|
||||
await retryPromise;
|
||||
console.log('✅ OCR retry with different language initiated');
|
||||
} catch (error) {
|
||||
console.log('ℹ️ Retry may have failed or timed out');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ No failed documents found for retry testing');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle mixed language document', async ({ adminPage: page }) => {
|
||||
// Upload mixed language document
|
||||
await page.goto('/upload');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
const fileInput = page.locator('input[type="file"]').first();
|
||||
|
||||
try {
|
||||
await fileInput.setInputFiles(MULTILINGUAL_TEST_FILES.mixed);
|
||||
|
||||
await expect(page.getByText('mixed_language_test.pdf')).toBeVisible({ timeout: 5000 });
|
||||
|
||||
const uploadButton = page.locator('button:has-text("Upload")').first();
|
||||
if (await uploadButton.isVisible()) {
|
||||
const uploadPromise = helpers.waitForApiCall('/api/documents', TIMEOUTS.upload);
|
||||
await uploadButton.click();
|
||||
await uploadPromise;
|
||||
|
||||
// Wait for OCR processing
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// Navigate to documents and check content
|
||||
await page.goto('/documents');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for the mixed document
|
||||
const mixedDocument = page.locator('text="mixed_language_test.pdf"').first();
|
||||
if (await mixedDocument.isVisible()) {
|
||||
await mixedDocument.click();
|
||||
|
||||
const contentArea = page.locator('.document-content, .ocr-text').first();
|
||||
if (await contentArea.isVisible({ timeout: TIMEOUTS.medium })) {
|
||||
const content = await contentArea.textContent();
|
||||
|
||||
if (content) {
|
||||
const hasSpanish = EXPECTED_CONTENT.mixed.spanish.some(word =>
|
||||
content.toLowerCase().includes(word.toLowerCase())
|
||||
);
|
||||
const hasEnglish = EXPECTED_CONTENT.mixed.english.some(word =>
|
||||
content.toLowerCase().includes(word.toLowerCase())
|
||||
);
|
||||
|
||||
if (hasSpanish && hasEnglish) {
|
||||
console.log('✅ Mixed language document processed successfully');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('ℹ️ Mixed language test file not found, skipping test');
|
||||
}
|
||||
});
|
||||
|
||||
test('should persist language preference across sessions', async ({ adminPage: page }) => {
|
||||
// Set language to Spanish
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
const languageSelector = page.locator('div:has(label:text("OCR Language"))').first();
|
||||
if (await languageSelector.isVisible()) {
|
||||
await languageSelector.click();
|
||||
|
||||
const spanishOption = page.locator('[data-value="spa"], li:has-text("Spanish")').first();
|
||||
if (await spanishOption.isVisible()) {
|
||||
await spanishOption.click();
|
||||
|
||||
const saveButton = page.locator('button:has-text("Save")').first();
|
||||
if (await saveButton.isVisible()) {
|
||||
await saveButton.click();
|
||||
await helpers.waitForToast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reload page to simulate new session
|
||||
await page.reload();
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Check if Spanish is still selected
|
||||
const currentLanguageIndicator = page.locator('text="Spanish", [data-value="spa"]').first();
|
||||
if (await currentLanguageIndicator.isVisible({ timeout: 5000 })) {
|
||||
console.log('✅ Language preference persisted across reload');
|
||||
} else {
|
||||
console.log('ℹ️ Could not verify language persistence');
|
||||
}
|
||||
});
|
||||
|
||||
test('should display available languages from API', async ({ adminPage: page }) => {
|
||||
// Navigate to settings and check API call for languages
|
||||
const languagesPromise = helpers.waitForApiCall('/api/ocr/languages', TIMEOUTS.medium);
|
||||
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
try {
|
||||
const languagesResponse = await languagesPromise;
|
||||
console.log('✅ OCR languages API called successfully');
|
||||
|
||||
// Check if language selector shows loading then options
|
||||
const languageSelector = page.locator('[data-testid="ocr-language-selector"]').first();
|
||||
if (await languageSelector.isVisible()) {
|
||||
// Click to see available options
|
||||
await languageSelector.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Count available language options
|
||||
const languageOptions = page.locator('li[role="option"], option[value]');
|
||||
const optionCount = await languageOptions.count();
|
||||
|
||||
if (optionCount > 0) {
|
||||
console.log(`✅ Found ${optionCount} language options in selector`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('ℹ️ Could not capture languages API call');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle bulk operations with multiple languages', async ({ adminPage: page }) => {
|
||||
await page.goto('/documents');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for documents and select multiple
|
||||
const documentCheckboxes = page.locator('.document-item input[type="checkbox"], [data-testid="document-checkbox"]');
|
||||
const checkboxCount = await documentCheckboxes.count();
|
||||
|
||||
if (checkboxCount > 1) {
|
||||
// Select first two documents
|
||||
await documentCheckboxes.nth(0).click();
|
||||
await documentCheckboxes.nth(1).click();
|
||||
|
||||
// Look for bulk action menu
|
||||
const bulkActionsMenu = page.locator('[data-testid="bulk-actions"], .bulk-actions, button:has-text("Bulk")').first();
|
||||
|
||||
if (await bulkActionsMenu.isVisible()) {
|
||||
await bulkActionsMenu.click();
|
||||
|
||||
// Look for language-specific bulk operations
|
||||
const bulkRetryWithLanguage = page.locator('button:has-text("Retry with Language"), .bulk-retry-language').first();
|
||||
|
||||
if (await bulkRetryWithLanguage.isVisible()) {
|
||||
await bulkRetryWithLanguage.click();
|
||||
|
||||
// Check for language selection in bulk retry
|
||||
const bulkLanguageSelector = page.locator('select, [role="combobox"]').first();
|
||||
if (await bulkLanguageSelector.isVisible()) {
|
||||
await bulkLanguageSelector.click();
|
||||
|
||||
const spanishBulkOption = page.locator('[data-value="spa"], option[value="spa"]').first();
|
||||
if (await spanishBulkOption.isVisible()) {
|
||||
await spanishBulkOption.click();
|
||||
|
||||
const confirmBulkButton = page.locator('button:has-text("Confirm"), button:has-text("Apply")').first();
|
||||
if (await confirmBulkButton.isVisible()) {
|
||||
const bulkRetryPromise = helpers.waitForApiCall('/bulk-retry', TIMEOUTS.ocr);
|
||||
await confirmBulkButton.click();
|
||||
|
||||
try {
|
||||
await bulkRetryPromise;
|
||||
console.log('✅ Bulk retry with Spanish language initiated');
|
||||
} catch (error) {
|
||||
console.log('ℹ️ Bulk retry may have failed or not available');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ Not enough documents for bulk operations test');
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle OCR language errors gracefully', async ({ adminPage: page }) => {
|
||||
await page.goto('/settings');
|
||||
await helpers.waitForLoadingToComplete();
|
||||
|
||||
// Look for language selector component
|
||||
const languageSelector = page.locator('[data-testid="ocr-language-selector"]').first();
|
||||
|
||||
// Check for error handling in language selector
|
||||
const errorAlert = page.locator('[role="alert"], .error, .alert-warning').first();
|
||||
const retryButton = page.locator('button:has-text("Retry"), .retry').first();
|
||||
|
||||
if (await errorAlert.isVisible()) {
|
||||
console.log('⚠️ Language selector showing error state');
|
||||
|
||||
if (await retryButton.isVisible()) {
|
||||
await retryButton.click();
|
||||
console.log('✅ Error retry mechanism available');
|
||||
}
|
||||
} else if (await languageSelector.isVisible()) {
|
||||
console.log('✅ Language selector loaded without errors');
|
||||
}
|
||||
|
||||
// Check for fallback behavior
|
||||
const englishFallback = page.locator('text="English (Fallback)"').first();
|
||||
if (await englishFallback.isVisible()) {
|
||||
console.log('✅ Fallback language option available');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -20,6 +20,13 @@ export const TEST_FILES = {
|
|||
test8: '../tests/test_images/test8.jpeg', // "Test 8\nThis is some text from text 8"
|
||||
test9: '../tests/test_images/test9.png', // "Test 9\nThis is some text from text 9"
|
||||
|
||||
// Multilingual test PDFs
|
||||
spanishTest: 'test_data/multilingual/spanish_test.pdf',
|
||||
englishTest: 'test_data/multilingual/english_test.pdf',
|
||||
mixedLanguageTest: 'test_data/multilingual/mixed_language_test.pdf',
|
||||
spanishComplex: 'test_data/multilingual/spanish_complex.pdf',
|
||||
englishComplex: 'test_data/multilingual/english_complex.pdf',
|
||||
|
||||
// Backwards compatibility
|
||||
image: '../tests/test_images/test1.png',
|
||||
multiline: '../tests/test_images/test2.jpg',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
# Multilingual OCR Test Files
|
||||
|
||||
This directory contains test files for validating the multiple OCR language capabilities of Readur.
|
||||
|
||||
## Test Files
|
||||
|
||||
### Spanish Test Files
|
||||
- **`spanish_test.pdf`** - Basic Spanish document with common words, accents, and phrases
|
||||
- **`spanish_complex.pdf`** - Complex Spanish document with special characters (ñ, ü, ¿, ¡)
|
||||
|
||||
### English Test Files
|
||||
- **`english_test.pdf`** - Basic English document with common words and technical terms
|
||||
- **`english_complex.pdf`** - Complex English document with contractions, hyphens, and abbreviations
|
||||
|
||||
### Mixed Language Test Files
|
||||
- **`mixed_language_test.pdf`** - Document containing both Spanish and English text sections
|
||||
|
||||
## Expected OCR Content
|
||||
|
||||
### Spanish Content Keywords
|
||||
- español, documento, reconocimiento
|
||||
- café, niño, comunicación, corazón
|
||||
- también, habitación, compañía
|
||||
- informática, educación, investigación
|
||||
|
||||
### English Content Keywords
|
||||
- English, document, recognition
|
||||
- technology, computer, software, hardware
|
||||
- testing, validation, verification, quality
|
||||
|
||||
### Mixed Content
|
||||
Both Spanish and English keywords should be recognized in the mixed language document.
|
||||
|
||||
## Usage in E2E Tests
|
||||
|
||||
These files are used by the `ocr-multiple-languages.spec.ts` test suite to validate:
|
||||
|
||||
1. **Language Selection**: Testing the OCR language selector component
|
||||
2. **Document Upload**: Uploading documents with specific language preferences
|
||||
3. **OCR Processing**: Validating OCR results contain expected language-specific content
|
||||
4. **Language Persistence**: Ensuring language preferences are saved across sessions
|
||||
5. **Retry Functionality**: Testing OCR retry with different languages
|
||||
6. **Error Handling**: Testing graceful fallback behavior
|
||||
|
||||
## Test Languages
|
||||
|
||||
- **Spanish (spa)**: Primary test language with accents and special characters
|
||||
- **English (eng)**: Secondary test language with technical terminology
|
||||
- **Auto-detect**: Testing automatic language detection
|
||||
|
||||
## File Creation
|
||||
|
||||
These files were created using the `create_multilingual_test_pdfs.py` script in the repository root.
|
||||
|
||||
To regenerate the test files:
|
||||
|
||||
```bash
|
||||
python3 create_multilingual_test_pdfs.py
|
||||
```
|
||||
|
||||
## OCR Language Testing Workflow
|
||||
|
||||
1. Set language preference in Settings page
|
||||
2. Upload test document with specific language content
|
||||
3. Wait for OCR processing to complete
|
||||
4. Validate OCR results contain expected keywords
|
||||
5. Test retry functionality with different languages
|
||||
6. Verify bulk operations work with multiple languages
|
||||
|
||||
## Expected Test Results
|
||||
|
||||
When OCR is configured correctly for Spanish (`spa`):
|
||||
- Spanish documents should have high recognition accuracy for accented characters
|
||||
- Phrases like "Hola mundo", "este es un documento", "en español" should be recognized
|
||||
- Special characters (ñ, ü, ¿, ¡) should be preserved
|
||||
|
||||
When OCR is configured correctly for English (`eng`):
|
||||
- English documents should have high recognition accuracy
|
||||
- Technical terms and abbreviations should be recognized
|
||||
- Phrases like "Hello world", "this is an English", "document" should be recognized
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If tests fail:
|
||||
|
||||
1. **Check Tesseract Installation**: Ensure Spanish language pack is installed
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install tesseract-ocr-spa
|
||||
|
||||
# macOS
|
||||
brew install tesseract-lang
|
||||
```
|
||||
|
||||
2. **Verify Language Availability**: Check `/api/ocr/languages` endpoint returns Spanish and English
|
||||
|
||||
3. **File Paths**: Ensure test files exist in the correct directory structure
|
||||
|
||||
4. **OCR Processing Time**: Allow sufficient timeout (120s) for OCR processing to complete
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
%PDF-1.3
|
||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Contents 7 0 R /MediaBox [ 0 0 612 792 ] /Parent 6 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20250714041906+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250714041906+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Count 1 /Kids [ 3 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 797
|
||||
>>
|
||||
stream
|
||||
Gas2HgMYb"%#46J'YN)FG#0s;LH+[,QDWuY?/kr)O;gVh*,W,p89.o&kssKFl2bs75USo@ae,@WGkf[0j9<>c"?rGVTKXV?T]%k`j9?CA_<ZC!SkmAT[r_CtLjb)KHNbD6F]%;\)&=^FV;9"Vk#ERh4,2f4,bOqKrtt#%-ZKB]V)tQa8GC&mWN9QsJ;ms>1;]X^bO.0<abs*?)9/5sb+:r"eY+5We4lI@PI*:YKBTZG4_SVb[;q%'TsDNT=ou.@:)(s1A+31BLNXNqGqJ6S0"4/%LN,IkP9S74gl:r<jL/O99]Meeo?hKQT@=u9RY37F&U^W)B/qq[$E8-MZ[,`?.-u%Yd5Zl4nZ>alEl!]t.O0Jpj[O9I32mmD+Z"Zu(rlC#2k4WGJiEF<.ZNSMAdpa8U>-\+k#(T,f4\>r<3VQ%o1,Ha%^ue(i&XfB8a7EIN&uH"la7<dak@(%VG^gWBN5l;'MfS=7%,ds$-X&&Ae142Q=neP0$b:9^-CH_'7uf.(Wl%R,*kk'=)>pCgrL+lSCFD!f#&?>-cb7VR1T..=LnBb]W:/m*J#:AqWI3gfTfju$#s#]CLK71$Ge:e:Xbm'a:M$k1'D)*dc^g78#9H2'Ca$sSRV:o7q-U'N!Obk:s]#0+S&S4`&gIUe1B_b5#C6ahUqFS0gSS?C/I*#V$$jPK9(B;(eJ<(6%FIUdOe[;nJ17/-L;T8/7@)GrVUZmbeVdbQgPkrh!*hjCF/rlD6`eA5:<&YCV^KF$@^Lh,8JP*'KZEcp%u9mf1)528bdYf,Jh3';)aSu~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000073 00000 n
|
||||
0000000104 00000 n
|
||||
0000000211 00000 n
|
||||
0000000404 00000 n
|
||||
0000000472 00000 n
|
||||
0000000768 00000 n
|
||||
0000000827 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<03c7beb85fc6d33c6a5f306170a12189><03c7beb85fc6d33c6a5f306170a12189>]
|
||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||
|
||||
/Info 5 0 R
|
||||
/Root 4 0 R
|
||||
/Size 8
|
||||
>>
|
||||
startxref
|
||||
1714
|
||||
%%EOF
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
%PDF-1.3
|
||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Contents 7 0 R /MediaBox [ 0 0 612 792 ] /Parent 6 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20250714041906+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250714041906+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Count 1 /Kids [ 3 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 835
|
||||
>>
|
||||
stream
|
||||
GasJP>>s91&;B$;(&J2W<#T*VLV@l?()SYA@]"S37BtILMCbU0S.L-tf2b5nR>O`p*_Z3UdQ'Ll!!`''\nb=IS>Pf;14UL#["'8_MBTZeMf`l!+*[=OU[`4cfH?=gP.%NFK*sQ'U+RV.g;:DGRL5<CK_-(VTDGpEXO<l'&kBQ#.5e36OZ<MJMAk^)'l6sn2[^W(]gA>*NB0tk(2mQ89In;?Wo$)X?S)uGcqSA2ReqBH\Duh-'cE@"Q/(lQU&fn7Sh"6'<q(M+E;''S#T7"=R?07^[==7TYu?"HX%Mj!OmK_\S&:sW]Z0fKT.l-a_K1ea,U"\'Q?p8W,d^`3Zo\K"0'nH7A3`6%gH.VO*pmWFcWFNsgZ`=SZ,2P0a8O8*r\@Sr.<=ia9/N@cL)^)/k`K.tH1A/Glb9?1BUhFX641cFH%)gRUh<[=7\CJ_fr3\6%X(95f:Qnocu2]Bm+Kht$Jh(n6m2m]eDf"c+8,QAj!H9"(bu@%@I10(#J6FGbcm?B;:(RshQgf7pQP:-*(;)"D'G*KCg2q#(Kg!X[?l;oYX1pL6@YAtfVuL2/"i!3Fl2,'^ER/eTD/F1Ea$4nSO965lLH]K8%bM5.G^ks)rhRcVg_Zk*,jOQJQ`]OF`"C8]$u_`WB1NBg<0(HgMd(p*[]/N/u:Xd[Va`i-!c!+Q,tANmD-U&"WBU?kU`IW6!2;+>8M\=l&Ei%j2Mfm0-?]&@HcDfD;2`cMo_C1[0*&G$nauhq;?=L"6[tOjI3pF`AD.F1OZcGnFQ(6_,Zq!gi_m."CpW;g#Rtq-[,urdrP^hdo-H8`2-j\.N>`ugTnkM]g'1Lb,>~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000073 00000 n
|
||||
0000000104 00000 n
|
||||
0000000211 00000 n
|
||||
0000000404 00000 n
|
||||
0000000472 00000 n
|
||||
0000000768 00000 n
|
||||
0000000827 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<5593a5d46848aedd233800654ecb7466><5593a5d46848aedd233800654ecb7466>]
|
||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||
|
||||
/Info 5 0 R
|
||||
/Root 4 0 R
|
||||
/Size 8
|
||||
>>
|
||||
startxref
|
||||
1752
|
||||
%%EOF
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
%PDF-1.3
|
||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Contents 7 0 R /MediaBox [ 0 0 612 792 ] /Parent 6 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20250714041906+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250714041906+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Count 1 /Kids [ 3 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 696
|
||||
>>
|
||||
stream
|
||||
Gas1]_2d5'&A@O6bd"8]9+imSNk;-SDUkc3EA*>.,[NJhNL`uH^106UCfM\kP+BgdpZB^FK,&g?n1-PM`tIf)B9;4O*["rc#3@Z-bId#1L&:O`HbI`pLeS?u?e<-+:IYa5C1lI%.]Jo(@`&cFa4%g=9Q.S!neK:Xo+q@?4_q=Z`"kR7#l.B"$OoX6HfT:iN7ts6S6$Kl:q=%FW;m,3onDS4!C7Ri,\dI;J*77<F.7p-[K0.SdWF82/J$JI(>:)$(%b%97`\=/I*_HKUbHMqeLKM-7"k;>3'nihX:RFqnWija]?44m)^^+W[W+XNoL-Q90+d(N<ba;X0\A@oAK$-=8U]cZ=$:72MZ:]PCs%Zu`Squ1Buhj*gZq=#gO^_`dce&BCr8S*fh.EDJR?V&f5_\Oi_nN=(6C@s8p=nY;%S!e9`i<4*]DlVZ:-J()d5n^Nh]B;("%3.]@#&b!8Er.e^/>B.tl'Tp55IHb#`YRe/;$0&WiJU1,g1&[W+?8n9d!>D0IZDG&cadXViRbJQS0.A@,1^Al)C'o.>lX1%!;oPZC\^Hji&7A$:mL>kn$b,:kNC#^b:j,8W9CP(."@!04ad2eh;6qWGqbP=L=n`V73Elrp1.28JHRjUY5If0Cm+Z=cta:;0^F\5hU\3K1X#/57%G7I.N.;S0L\4JZQL5&C`lR0_+6_;/I3d?5s~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000073 00000 n
|
||||
0000000104 00000 n
|
||||
0000000211 00000 n
|
||||
0000000404 00000 n
|
||||
0000000472 00000 n
|
||||
0000000768 00000 n
|
||||
0000000827 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<bb507ce670e7a3b55e235692958519e1><bb507ce670e7a3b55e235692958519e1>]
|
||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||
|
||||
/Info 5 0 R
|
||||
/Root 4 0 R
|
||||
/Size 8
|
||||
>>
|
||||
startxref
|
||||
1613
|
||||
%%EOF
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
%PDF-1.3
|
||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Contents 7 0 R /MediaBox [ 0 0 612 792 ] /Parent 6 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20250714041906+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250714041906+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Count 1 /Kids [ 3 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 716
|
||||
>>
|
||||
stream
|
||||
Gas2H9lJc?%#46L(#3XFK$<duR`g4mfr<V]2:s>o7V2`;[U_pP&a\,1P+7SA;\;8K)=%n-nhgS]r"EM(4i!C)I/50TjKNt8;(bMo4hu5bGKuNu?S>L4]U0:*%ZTAt_u$W,:-Ijh^t&t)05=oVfU.-Okf%q"h$`R5hGXApg'tH-,'AMRj[aQ%<0\Df6jMcWOpX9kcr0&Eq6]hPVqf^X3m`<n)>9cb676TsX^PS$@_lgpf?1:>iR]DP"2u_i+)&"ejNj_]BGCkW>SOsWi1+iH-"GEF/;lP=?]-5.\@=ih,8X6-bm:;c1X4u^Z8'qF0hJM41/OUm;&_/*%$SH;oVE?"7a/5c<>j?Ak=tP]l,WjBK/=M%Ko,ucEO,@":!$HmV%h[Jm',XsN2@<R?'81D7rLM!LBfo-,#*P`qMG]q_`)I52GomUBH!P-UlIT($[uX(Z.e@$h/FWVdYOp;,$K##Xm%n'CV?\OeXAq2QT^]g%4P=Z#hM4RG59o7[SPX.XW4t>P#D/0f_ukg\QMnr]/Ob+QIRk$AVo:1Fg6h(#]=eqK2mB`!qHm`hg9jP,)#ks9)^coWhV[#$a7%rf2D8_(Gi8Od$8Pn-aK-3TB8/'7MkT)ft*<=]hG$6:38bb&0qae:lB1X&O/0<e#7P@%6S@?&pqaiXgf%1K+U86?o0J$f,9pnjS)M23=d\;B2GR.R3$;/ea`nKGCKVjJqfB~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000073 00000 n
|
||||
0000000104 00000 n
|
||||
0000000211 00000 n
|
||||
0000000404 00000 n
|
||||
0000000472 00000 n
|
||||
0000000768 00000 n
|
||||
0000000827 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<fc419b963ba2bb437952c7c5c6f73e95><fc419b963ba2bb437952c7c5c6f73e95>]
|
||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||
|
||||
/Info 5 0 R
|
||||
/Root 4 0 R
|
||||
/Size 8
|
||||
>>
|
||||
startxref
|
||||
1633
|
||||
%%EOF
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
%PDF-1.3
|
||||
%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com
|
||||
1 0 obj
|
||||
<<
|
||||
/F1 2 0 R
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Contents 7 0 R /MediaBox [ 0 0 612 792 ] /Parent 6 0 R /Resources <<
|
||||
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
|
||||
>> /Rotate 0 /Trans <<
|
||||
|
||||
>>
|
||||
/Type /Page
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
/PageMode /UseNone /Pages 6 0 R /Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Author (anonymous) /CreationDate (D:20250714041906+00'00') /Creator (ReportLab PDF Library - www.reportlab.com) /Keywords () /ModDate (D:20250714041906+00'00') /Producer (ReportLab PDF Library - www.reportlab.com)
|
||||
/Subject (unspecified) /Title (untitled) /Trapped /False
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/Count 1 /Kids [ 3 0 R ] /Type /Pages
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<<
|
||||
/Filter [ /ASCII85Decode /FlateDecode ] /Length 756
|
||||
>>
|
||||
stream
|
||||
Gas2H_/A!]%#45!$6Is<a<Jg4dp30b9<gjn'n_R]a>24XYbXOmK;D@.b"?-PR"><m7>Du,T;3;Tl,UfAe,U"*I"#P?;CK#iZ:+uZ1Mk4'ZTT8pq-lM?fa1NO:j>26\)48,1n*8fKCIk)X9G8J9t(oRc^AY=T_noRV`FQ;&SL1;#itH_'qbf\fEAGEOWH1nBJ'<7$spe%hT)&UGWt[VShYL`CO1[=TuXni@B^PC^u!aJ\2]hW7kV7.OD%ZEX[Y-5pqBgMe3jpAp?3q%lLriin*2Nsl=q>3Ihj'$d#-Q)M__RYNg-/tT&*?,g4"l7nn_i9X=,Ll[c/g'J5KffM0C#"P!XHs2r<K2g\;g[B).,HKs$\rP4)/qWh6&.:lmlE6l_Pr-_)qrlZC*%7Y1s3F>M.(;s$?I=0H70PaU>4jaL7C82WGkkm1LX3th$gCZE9C`Cr7WEfcua#?3N/^%4+*EUVFO,Cbf-ka`.kl5=_aTN=EnFf;H^pe2[9ne7gQHQ,I(?)Cc[0jRP7SGE1DMSe]5iQ7KXSbY=rr^6jD>o44@i`bk7<;U,FCrO6%KJmreqj"$aM(\<ANG(_-fQhY?1bFSThd(*tT)2B8JTeHs$9s7ie>-Jm`a5A/;#UQr9R8YX#Z;QuSLL[I\-AiU/aEnMd*hfaa/4Pc;(c#PXY/.'j*B>*q:2/@/HdpHj/aG<e_A)uMp,NkBN4T^ek%KphUjf)'i\uI:eb"!;31oqdEhdUU6q8P2u`ji5bqo~>endstream
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000073 00000 n
|
||||
0000000104 00000 n
|
||||
0000000211 00000 n
|
||||
0000000404 00000 n
|
||||
0000000472 00000 n
|
||||
0000000768 00000 n
|
||||
0000000827 00000 n
|
||||
trailer
|
||||
<<
|
||||
/ID
|
||||
[<777fc4a89fb76bdddd9c05de1a073282><777fc4a89fb76bdddd9c05de1a073282>]
|
||||
% ReportLab generated PDF document -- digest (http://www.reportlab.com)
|
||||
|
||||
/Info 5 0 R
|
||||
/Root 4 0 R
|
||||
/Size 8
|
||||
>>
|
||||
startxref
|
||||
1673
|
||||
%%EOF
|
||||
Loading…
Reference in New Issue