Update matching logic: AI scores all candidates, lower threshold, absolute amount, prompt improvements

This commit is contained in:
Iyeoluwa Akinrinola
2025-07-02 16:38:01 +01:00
commit a519c42866
10641 changed files with 3944174 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+31
View File
@@ -0,0 +1,31 @@
# PROJECT OVERVIEW
The AI Bookkeeper app helps businesses (especially small businesses and accounting firms) streamline the financial reconciliation process by using AI to match receipts with transactions. It reduces manual bookkeeping work by:
- Automatically importing receipts and transaction data
- Using AI to match corresponding records
- Allowing users to review, approve, and sync reconciled data to QuickBooks
# PERSONLITY
Teach me like a senior developer would
# TECH STACK
Python
# ERROR FIXING PROCESS
Step 1: Explain the error in simple terms
Step 2: Explain the solution in simple terms
Step 3: Show how to fix the error in simple terms.
# BUILDING PROCESS
Step 1: Explain the build process in simple terms
Step 2: Explain the solution in simple terms
Step 3: Show how to build the project
# GITHUB PUSH PROCESS
git add .
git commit -m "message"
Step 3: Show how to build the project
# IMPORTANT
Do not try to add extra features. Let your codes be straight to the point
+220
View File
@@ -0,0 +1,220 @@
# AI Bookkeeper - Data Science Engine
AI-powered receipt-to-transaction matching engine using Groq LLM. This is a **Data Science Engine** that provides intelligent matching capabilities for backend applications.
## 🎯 Purpose
This Data Science Engine receives QuickBooks transaction data from backend applications and provides:
- **AI-powered receipt processing** (OCR and data extraction)
- **Intelligent receipt-transaction matching** with confidence scores
- **Google Drive integration** for batch receipt processing
- **Configurable AI rules** for business logic
- **Feedback logging** for continuous improvement
## 🚀 Quick Start
### 1. Install Dependencies
```bash
pip install -r requirements.txt
```
### 2. Configure API Keys
The Groq API key is already configured in `config.py`
### 3. Start the DS Engine
```bash
python main.py
```
### 4. Access API Documentation
- **Swagger UI**: http://localhost:8343/docs
- **ReDoc**: http://localhost:8343/redoc
## 📋 API Endpoints
### QuickBooks Data Import
- `POST /transactions/import/quickbooks` - Import and convert QuickBooks transactions
### Receipt Processing
- `POST /upload` - Upload receipt documents (PDF/images)
- `POST /process/{file_id}` - Extract data from uploaded documents
- `GET /documents` - List all processed documents
### Google Drive Integration
- `POST /drive/sync` - Sync and process receipts from Google Drive
- `GET /drive/folders` - List accessible Google Drive folders
- `GET /drive/folder/{folder_id}` - Get folder information
### AI Matching Engine
- `POST /match` - Match receipts to transactions using AI
- `POST /approve` - Approve or reject AI matches
### AI Rules Management
- `POST /rules` - Add new AI rules
- `GET /rules` - List all active rules
- `DELETE /rules/{rule_name}` - Delete rules
### System Monitoring
- `GET /stats` - Get system statistics and performance metrics
## 🔧 Core Components
### **AIMatcher** (`ai_matcher.py`)
- Uses Groq LLM to compare receipts and transactions
- Provides confidence scores and reasoning
- Configurable matching criteria (amount, date, vendor)
### **AIRulesEngine** (`ai_rules.py`)
- Applies business rules for auto-approval and categorization
- Configurable rule conditions and actions
- Supports system and user-generated rules
### **DocumentProcessor** (`document_processor.py`)
- AI-powered receipt data extraction
- Supports PDF and image formats
- Uses Groq vision model for OCR
### **MatchingEngine** (`matching_engine.py`)
- Main orchestrator combining all components
- Handles the complete matching workflow
- Provides statistics and feedback logging
### **FeedbackLogger** (`feedback_logger.py`)
- Tracks manual overrides for AI training
- Maintains audit trail of user decisions
- Enables continuous model improvement
## 📊 Configuration
Edit `config.py` to adjust:
- **Confidence threshold** (default: 0.8)
- **Date tolerance days** (default: 7)
- **Amount tolerance percent** (default: 5%)
- **Groq API key** (already configured)
## 🔄 Integration Workflow
### 1. Backend Sends QuickBooks Data
```python
# Backend sends QuickBooks transactions
response = requests.post(
"http://localhost:8343/transactions/import/quickbooks",
json={
"transactions": [
{
"id": "QB_TXN_123",
"txn_date": "2024-01-15",
"amount": 12.50,
"payee_name": "Starbucks",
"memo": "Coffee purchase"
}
]
}
)
```
### 2. Process Receipts
```python
# Sync from Google Drive
response = requests.post(
"http://localhost:8343/drive/sync",
json={"folder_id": "your_folder_id"}
)
# Or upload directly
response = requests.post(
"http://localhost:8343/upload",
files={"file": receipt_file}
)
```
### 3. AI Matching
```python
# Match receipts to transactions
response = requests.post(
"http://localhost:8343/match",
json={
"receipts": processed_receipts,
"transactions": converted_transactions
}
)
```
### 4. User Feedback
```python
# Approve or reject matches
response = requests.post(
"http://localhost:8343/approve",
json={
"match_id": "match_123",
"user_id": "user_456",
"action": "approve"
}
)
```
## 🎯 Key Features
- **AI-powered matching** with confidence scores
- **Rule-based auto-approval** and categorization
- **Feedback logging** for continuous improvement
- **Configurable matching parameters**
- **Google Drive integration** for batch processing
- **JSON API** for easy backend integration
- **Comprehensive error handling**
## 📝 Data Formats
### QuickBooks Transaction Input
```json
{
"id": "string",
"txn_date": "YYYY-MM-DD",
"amount": 0.00,
"payee_name": "string",
"memo": "string (optional)",
"account_name": "string (optional)",
"txn_type": "string (optional)"
}
```
### Match Result Output
```json
{
"receipt_id": "string",
"transaction_id": "string",
"confidence_score": 0.95,
"match_reason": "string",
"receipt_vendor": "string",
"receipt_amount": 0.00,
"transaction_vendor": "string",
"transaction_amount": 0.00
}
```
## 🔍 AI Matching Criteria
The engine uses three primary criteria for matching:
1. **Amount Similarity** - Compares receipt and transaction amounts (5% tolerance)
2. **Date Proximity** - Checks date closeness (7-day tolerance)
3. **Vendor Matching** - AI-powered vendor name comparison
## 🚀 Production Deployment
For production deployment:
- Replace in-memory storage with a database
- Configure proper authentication
- Set up monitoring and logging
- Use environment variables for configuration
- Implement proper error handling and retries
## 📞 Support
This Data Science Engine is designed to be integrated with backend applications that handle:
- QuickBooks API connections
- User interface and workflows
- Data persistence and management
- External integrations
The engine focuses purely on AI/ML capabilities and provides a clean JSON API for backend integration.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+102
View File
@@ -0,0 +1,102 @@
import groq
from datetime import datetime, timedelta
from typing import List, Tuple
import config
from models import Receipt, Transaction, Match
class AIMatcher:
def __init__(self):
self.client = groq.Groq(api_key=config.GROQ_API_KEY)
self.model = "llama3-8b-8192"
def match_receipts_to_transactions(self, receipts: List[Receipt], transactions: List[Transaction]) -> List[Match]:
matches = []
for receipt in receipts:
best_match = self._find_best_match(receipt, transactions)
if best_match:
matches.append(best_match)
return sorted(matches, key=lambda x: x.confidence_score, reverse=True)
def _find_best_match(self, receipt: Receipt, transactions: List[Transaction]) -> Match:
candidates = self._filter_candidates(receipt, transactions)
if not candidates:
return None
best_match = None
highest_score = 0
for transaction in candidates:
score, reason = self._calculate_match_score(receipt, transaction)
if score > highest_score and score >= config.CONFIDENCE_THRESHOLD:
highest_score = score
best_match = Match(receipt, transaction, score, reason)
return best_match
def _filter_candidates(self, receipt: Receipt, transactions: List[Transaction]) -> List[Transaction]:
# Return ALL transactions - let the AI decide on scoring
# Only filter out transactions with completely different amounts (>50% difference) to avoid obvious mismatches
candidates = []
amount_threshold = receipt.amount * 0.5 # 50% threshold for obvious mismatches
for transaction in transactions:
# Use absolute value for transaction amount comparison
transaction_amount_abs = abs(transaction.amount)
# Only exclude transactions with obviously different amounts
if abs(receipt.amount - transaction_amount_abs) <= amount_threshold:
candidates.append(transaction)
return candidates
def _calculate_match_score(self, receipt: Receipt, transaction: Transaction) -> Tuple[float, str]:
# Calculate differences for the AI to consider
date_diff = abs((receipt.receipt_date - transaction.transaction_date).days)
transaction_amount_abs = abs(transaction.amount)
amount_diff = abs(receipt.amount - transaction_amount_abs)
amount_percent_diff = (amount_diff / receipt.amount) * 100 if receipt.amount > 0 else 0
prompt = f"""
Compare this receipt with this transaction and provide a confidence score (0-1) and brief reason:
Receipt: {receipt.vendor}, ${receipt.amount}, {receipt.receipt_date.strftime('%Y-%m-%d')}
Transaction: {transaction.vendor}, ${transaction.amount} (absolute: ${transaction_amount_abs}), {transaction.transaction_date.strftime('%Y-%m-%d')}
Differences:
- Date difference: {date_diff} days
- Amount difference: ${amount_diff} ({amount_percent_diff:.1f}%)
- Vendor comparison: "{receipt.vendor}" vs "{transaction.vendor}"
Scoring guidelines:
- Perfect matches (same vendor, amount, date): 0.95-1.0
- High confidence (minor differences): 0.8-0.94
- Medium confidence (moderate differences): 0.6-0.79
- Low confidence (significant differences): 0.4-0.59
- Very low confidence (major differences): 0.2-0.39
- No match: 0.0-0.19
Consider vendor name similarity, amount accuracy, and date proximity.
Score based on your discretion - even imperfect matches should get scores if there's reasonable similarity.
Return only: score|reason
"""
try:
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=100,
temperature=0.1
)
result = response.choices[0].message.content.strip()
if '|' in result:
score_str, reason = result.split('|', 1)
score = float(score_str.strip())
return min(max(score, 0), 1), reason.strip()
else:
return 0.0, "Invalid AI response"
except Exception as e:
return 0.0, f"AI error: {str(e)}"
+63
View File
@@ -0,0 +1,63 @@
from dataclasses import dataclass
from typing import Dict, Any, List
import config
from models import Receipt, Transaction
@dataclass
class AIRule:
name: str
condition: str
action: str
source: str
status: str = "active"
class AIRulesEngine:
def __init__(self):
self.rules: List[AIRule] = []
self._load_default_rules()
def _load_default_rules(self):
self.rules = [
AIRule("exact_amount_match", "amount_diff <= 0.01", "auto_approve", "system"),
AIRule("same_vendor_same_date", "vendor_match and date_diff <= 1", "high_confidence", "system"),
AIRule("gas_station_pattern", "vendor contains 'gas' or 'fuel'", "categorize_transport", "system")
]
def apply_rules(self, receipt: Receipt, transaction: Transaction) -> Dict[str, Any]:
results = {"auto_approve": False, "confidence_boost": 0, "category": None}
for rule in self.rules:
if rule.status != "active":
continue
if self._evaluate_condition(rule.condition, receipt, transaction):
self._execute_action(rule.action, results, receipt, transaction)
return results
def _evaluate_condition(self, condition: str, receipt: Receipt, transaction: Transaction) -> bool:
amount_diff = abs(receipt.amount - transaction.amount)
date_diff = abs((receipt.receipt_date - transaction.transaction_date).days)
vendor_match = receipt.vendor.lower() in transaction.vendor.lower() or transaction.vendor.lower() in receipt.vendor.lower()
return eval(condition, {
"amount_diff": amount_diff,
"date_diff": date_diff,
"vendor_match": vendor_match,
"receipt": receipt,
"transaction": transaction
})
def _execute_action(self, action: str, results: Dict[str, Any], receipt: Receipt, transaction: Transaction):
if action == "auto_approve":
results["auto_approve"] = True
elif action == "high_confidence":
results["confidence_boost"] += 0.2
elif action == "categorize_transport":
results["category"] = "Transportation"
def add_rule(self, rule: AIRule):
self.rules.append(rule)
def remove_rule(self, rule_name: str):
self.rules = [r for r in self.rules if r.name != rule_name]
+101
View File
@@ -0,0 +1,101 @@
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional
class ReceiptRequest(BaseModel):
id: str
file_name: str
upload_date: datetime
receipt_date: datetime
amount: float
tax: float
vendor: str
category: str
class TransactionRequest(BaseModel):
id: str
transaction_date: datetime
amount: float
vendor: str
notes: str
# New: QuickBooks specific models
class QuickBooksTransaction(BaseModel):
"""Raw QuickBooks transaction data"""
id: str
txn_date: str # QuickBooks date format
amount: float
payee_name: str # QuickBooks vendor field
memo: Optional[str] = None
account_name: Optional[str] = None
txn_type: Optional[str] = None
class QuickBooksImportRequest(BaseModel):
"""Request to import QuickBooks transactions"""
transactions: List[QuickBooksTransaction]
date_range: Optional[dict] = None
account_filter: Optional[str] = None
class QuickBooksImportResponse(BaseModel):
"""Response from QuickBooks import"""
imported_count: int
converted_transactions: List[TransactionRequest]
errors: List[str] = []
class MatchResponse(BaseModel):
receipt_id: str
transaction_id: str
confidence_score: float
match_reason: str
receipt_vendor: str
receipt_amount: float
transaction_vendor: str
transaction_amount: float
class MatchingRequest(BaseModel):
receipts: List[ReceiptRequest]
transactions: List[TransactionRequest]
class MatchingResponse(BaseModel):
matches: List[MatchResponse]
stats: dict
class ApprovalRequest(BaseModel):
match_id: str
user_id: str
action: str # "approve" or "reject"
reason: Optional[str] = None
class RuleRequest(BaseModel):
name: str
condition: str
action: str
source: str = "user"
class DocumentUploadResponse(BaseModel):
file_id: str
filename: str
file_type: str
upload_date: datetime
status: str
class DocumentProcessResponse(BaseModel):
file_id: str
extraction_success: bool
vendor: Optional[str] = None
total_amount: Optional[float] = None
tax_amount: Optional[float] = None
date: Optional[str] = None
category: Optional[str] = None
confidence: Optional[float] = None
error: Optional[str] = None
class DriveSyncRequest(BaseModel):
folder_id: Optional[str] = None
auto_process: bool = True
class DriveSyncResponse(BaseModel):
files_processed: int
successful_extractions: int
failed_extractions: int
results: List[DocumentProcessResponse]
+516
View File
@@ -0,0 +1,516 @@
Account Type,Account Number,Transaction Date,Cheque Number,Description 1,Description 2,Amount
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,2121.2
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,5376.41
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,1112.29
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,918.99
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,20559.97
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,1772.18
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,416.09
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,6960.58
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,416.09
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,694.58
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,1669.29
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,5415.81
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,572.39
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,2149.83
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,3583.87
Chequing,20002-17758460,1/2/2025,,Misc Payment,MK INC,2783.25
Chequing,20002-17758460,1/2/2025,,Online transfer sent - 2155,Findlay Wong,-3872.71
Chequing,20002-17758460,1/2/2025,,Regular transaction fee,1 Dr @ 2.50,-2.5
Chequing,20002-17758460,1/2/2025,,Monthly fee,,-6
Chequing,20002-17758460,1/2/2025,,Bill Payment,PAY-FILE FEES,-4
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-192.77
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-188.77
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-270.94
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-446
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-314.1
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-245.93
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-224.49
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-287.98
Chequing,20002-17758460,1/6/2025,,Online Banking transfer - 9316,,-1413.24
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-192.77
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-560.99
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-366.99
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-237.99
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-132.7
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-392.1
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-696.66
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-288.34
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-661.49
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-389.99
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,,-431.71
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-176.8
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-196.99
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-997.35
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-418.05
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-518.81
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-392.1
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-667.37
Chequing,20002-17758460,1/6/2025,,e-Transfer sent,Remitbee,-236.46
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/6/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/7/2025,,e-Transfer sent,Remitbee,-6494.44
Chequing,20002-17758460,1/7/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/8/2025,,Insurance,THE EMPIRE LIFE,-2200.95
Chequing,20002-17758460,1/9/2025,,Misc Payment,MK INC,7164.85
Chequing,20002-17758460,1/9/2025,,Misc Payment,MK INC,851.05
Chequing,20002-17758460,1/9/2025,,Misc Payment,MK INC,15774.65
Chequing,20002-17758460,1/9/2025,,COMMERCIAL TAXES,EMPTX 8584774,-2577.54
Chequing,20002-17758460,1/9/2025,,COMMERCIAL TAXES,EMPTX 8584114,-2577.54
Chequing,20002-17758460,1/10/2025,,Misc Payment,MK INC,6711.7
Chequing,20002-17758460,1/10/2025,,ATM deposit - TY916139,,2252.25
Chequing,20002-17758460,1/10/2025,,ATM/Mobile adjustment credit,,916.67
Chequing,20002-17758460,1/14/2025,,Misc Payment,STRIPE,4363.08
Chequing,20002-17758460,1/16/2025,,e-Transfer sent,Remitbee,-71.59
Chequing,20002-17758460,1/16/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/16/2025,,COMMERCIAL TAXES,EMPTX 5448200,-414.5
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-716.08
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-191.1
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-311.36
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-246.46
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-156.3
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-224.98
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-644.47
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-182.81
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-189.17
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,,-191.1
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-432.64
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-999.53
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-418.75
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-446.96
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-132.91
Chequing,20002-17758460,1/20/2025,,ATM deposit - TY916817,,640
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-519.7
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-234.99
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-271.52
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-668.82
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-371.99
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-236.85
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-392.76
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-392.76
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-288.82
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-198.99
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-567.99
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-698.18
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-240.99
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-662.61
Chequing,20002-17758460,1/20/2025,,e-Transfer sent,Remitbee,-672.95
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/20/2025,,Misc Payment,STRIPE,-12.77
Chequing,20002-17758460,1/21/2025,,Misc Payment,STRIPE,2194.16
Chequing,20002-17758460,1/22/2025,,e-Transfer sent,Ajai Srivastava,-565
Chequing,20002-17758460,1/22/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/23/2025,,Misc Payment,STRIPE,-31.92
Chequing,20002-17758460,1/24/2025,,Misc Payment,CITY OF RICHMON,282.5
Chequing,20002-17758460,1/24/2025,,Misc Payment,STRIPE,5485.85
Chequing,20002-17758460,1/27/2025,,e-Transfer sent,Susan Lee,-160
Chequing,20002-17758460,1/27/2025,,Online Banking transfer - 3607,,-41.79
Chequing,20002-17758460,1/27/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/30/2025,,e-Transfer sent,,-3872.71
Chequing,20002-17758460,1/30/2025,,e-Transfer sent,Sok Kuan Mark,-2100
Chequing,20002-17758460,1/30/2025,,e-Transfer sent,Sok Kuan Mark,-1200
Chequing,20002-17758460,1/30/2025,,Online Banking transfer - 1072,,-3.5
Chequing,20002-17758460,1/30/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/30/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/30/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,1/31/2025,,Online transfer sent - 5947,ryan wong,-3872.71
Chequing,20002-17758460,1/31/2025,,Online Banking transfer - 5870,,-1649.19
Chequing,20002-17758460,1/31/2025,,Misc Payment,STRIPE,59.56
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-336.32
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-451.56
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-283.35
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-698.28
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-672.86
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-1046.67
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-519.28
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-466.51
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-728.93
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-206.32
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-257.18
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-747.62
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-206.32
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-283.35
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-234.75
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-290.3
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-522.43
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-133.58
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-373.99
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-200.99
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-133.58
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-154.48
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-394.77
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-571.99
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-420.89
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-394.77
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-242.99
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-395.07
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-389.99
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-666.51
Chequing,20002-17758460,2/3/2025,,e-Transfer sent,Remitbee,-674.5
Chequing,20002-17758460,2/3/2025,,Monthly fee,,-6
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/3/2025,,Bill Payment,PAY-FILE FEES,-6
Chequing,20002-17758460,2/5/2025,,Misc Payment,STRIPE,122.37
Chequing,20002-17758460,2/6/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,2/7/2025,,e-Transfer sent,Remitbee,-173.94
Chequing,20002-17758460,2/7/2025,,e-Transfer received,ANDREA IVANKA INTERNATIONAL IN,120
Chequing,20002-17758460,2/7/2025,,Online Banking transfer - 1371,,-0.04
Chequing,20002-17758460,2/7/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/7/2025,,Misc Payment,STRIPE,314.05
Chequing,20002-17758460,2/10/2025,,Misc Payment,STRIPE,179.03
Chequing,20002-17758460,2/10/2025,,ATM deposit - TY901067,,2252.25
Chequing,20002-17758460,2/10/2025,,Insurance,EMP LIFE,-2200.95
Chequing,20002-17758460,2/10/2025,,ATM/Mobile adjustment credit,,898.2
Chequing,20002-17758460,2/11/2025,,COMMERCIAL TAXES,EMPTX 1134350,-2539.4
Chequing,20002-17758460,2/12/2025,,Misc Payment,STRIPE,125.62
Chequing,20002-17758460,2/13/2025,,Misc Payment,STRIPE,304.65
Chequing,20002-17758460,2/14/2025,,Misc Payment,STRIPE,314.05
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-330.09
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-269.42
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-202.52
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-269.42
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-223.24
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-710.47
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-639.43
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-156.35
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-202.52
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-244.55
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-429.28
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-692.71
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-663.58
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-443.48
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-559
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-196.99
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,,-130.34
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-366.99
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-389.99
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-237.99
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-994.67
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-130.35
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-385.06
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-823.72
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-649.49
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-385.04
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-308.64
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-283.17
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-560.32
Chequing,20002-17758460,2/17/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/18/2025,,Misc Payment,STRIPE,125.62
Chequing,20002-17758460,2/18/2025,,e-Transfer sent,Ajai Srivastava,-1412.5
Chequing,20002-17758460,2/18/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,2/19/2025,,Misc Payment,STRIPE,188.43
Chequing,20002-17758460,2/20/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,2/21/2025,,Misc Payment,STRIPE,358.06
Chequing,20002-17758460,2/24/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,2/25/2025,,Misc Payment,STRIPE,-30.32
Chequing,20002-17758460,2/26/2025,,Misc Payment,STRIPE,5397.47
Chequing,20002-17758460,2/27/2025,,Misc Payment,STRIPE,304.65
Chequing,20002-17758460,2/28/2025,,Matured GIC,,100000
Chequing,20002-17758460,2/28/2025,,GIC interest,,4500
Chequing,20002-17758460,2/28/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-735.5
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-367.99
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-278.8
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-658.72
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-417.7
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-314.02
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-132.59
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,,-196.99
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-686.95
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-237.99
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-717.11
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-559
Chequing,20002-17758460,3/3/2025,,Misc Payment,STRIPE,1137.39
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-459
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-1024.68
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-444.29
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-832.17
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-518.1
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-660.58
Chequing,20002-17758460,3/3/2025,,Online transfer sent - 5372,ryan wong,-3872.71
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-391.55
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Remitbee,-391.57
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Susan Lee,-160
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Sok Kuan Mark,-2100
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Chuck Windley,-3872.71
Chequing,20002-17758460,3/3/2025,,e-Transfer sent,Sok Kuan Mark,-1200
Chequing,20002-17758460,3/3/2025,,Online Banking transfer - 4576,,-3035.8
Chequing,20002-17758460,3/3/2025,,Online Banking transfer - 3265,,-42.34
Chequing,20002-17758460,3/3/2025,,Monthly fee,,-6
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/3/2025,,COMMERCIAL TAXES,EMPTX 3123370,-2539.4
Chequing,20002-17758460,3/3/2025,,Bill Payment,PAY-FILE FEES,-2
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-154.06
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-278.8
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-234.99
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-253.06
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-146.25
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-363.38
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-346.13
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-212.3
Chequing,20002-17758460,3/4/2025,,e-Transfer sent,Remitbee,-212.3
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/4/2025,,Misc Payment,STRIPE,2742.77
Chequing,20002-17758460,3/7/2025,,e-Transfer sent,Remitbee,-71.3
Chequing,20002-17758460,3/7/2025,,e-Transfer received,AJAI KUMARSRIVASTAVA,1412.5
Chequing,20002-17758460,3/7/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/10/2025,,Misc Payment,STRIPE,116.22
Chequing,20002-17758460,3/10/2025,,Online Banking payment - 7743,VISA - BNS,-224.76
Chequing,20002-17758460,3/10/2025,,Online Banking transfer - 2890,,-0.06
Chequing,20002-17758460,3/10/2025,,Insurance,EMP LIFE,-2200.95
Chequing,20002-17758460,3/11/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,3/12/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,3/13/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,3/14/2025,,Misc Payment,STRIPE,187.91
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-363.58
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-278.95
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-717.51
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-278.95
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,,-662.31
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-564
Chequing,20002-17758460,3/17/2025,,Misc Payment,STRIPE,376.86
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-253.2
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-687.33
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-198.99
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-333.18
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,,-459.25
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-444.53
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-370.99
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-239.99
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-1471.81
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-415.14
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-515.13
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-831.48
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-389.34
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-515.07
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-85.4
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-389.99
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-1969.39
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-131.76
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,Remitbee,-131.79
Chequing,20002-17758460,3/17/2025,,e-Transfer sent,,-312.03
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/17/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/18/2025,,e-Transfer sent,,-157
Chequing,20002-17758460,3/18/2025,,e-Transfer sent,Remitbee,-204.41
Chequing,20002-17758460,3/18/2025,,e-Transfer sent,,-204.41
Chequing,20002-17758460,3/18/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/18/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/18/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/18/2025,,Misc Payment,STRIPE,430.27
Chequing,20002-17758460,3/19/2025,,Misc Payment,STRIPE,62.81
Chequing,20002-17758460,3/20/2025,,Misc Payment,STRIPE,669.92
Chequing,20002-17758460,3/21/2025,,Misc Payment,STRIPE,420.87
Chequing,20002-17758460,3/24/2025,,Misc Payment,STRIPE,188.43
Chequing,20002-17758460,3/25/2025,,Misc Payment,STRIPE,188.43
Chequing,20002-17758460,3/26/2025,,Online Banking transfer - 8674,,-42.73
Chequing,20002-17758460,3/26/2025,,e-Transfer sent,Sok Kuan Mark,-2100
Chequing,20002-17758460,3/26/2025,,e-Transfer sent,Sok Kuan Mark,-1200
Chequing,20002-17758460,3/26/2025,,e-Transfer sent,Susan Lee,-160
Chequing,20002-17758460,3/26/2025,,Online Banking transfer - 9723,,-42.73
Chequing,20002-17758460,3/26/2025,,Online Banking transfer - 1905,,-4653.31
Chequing,20002-17758460,3/26/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/26/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/26/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/27/2025,,Misc Payment,STRIPE,429.31
Chequing,20002-17758460,3/28/2025,,Online transfer sent - 9210,ryan wong,-3872.71
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,1711.73
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,7135.05
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,13225.25
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,16739.66
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,4460.31
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,1723.59
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,2844.67
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,2816.13
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,1661.4
Chequing,20002-17758460,3/28/2025,,Misc Payment,MK INC,1464.54
Chequing,20002-17758460,3/28/2025,,e-Transfer sent,Chuck Windley,-3872.71
Chequing,20002-17758460,3/28/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/28/2025,,Misc Payment,STRIPE,2826.11
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-703.67
Chequing,20002-17758460,3/31/2025,,Misc Payment,STARTUP SLANG I,1000
Chequing,20002-17758460,3/31/2025,,Misc Payment,STRIPE,4425.37
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-674.08
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-198.99
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-240.99
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-131.12
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-1025.07
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-1000
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-370.99
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-310.55
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-1025.07
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-653.48
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-649.54
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-1443.42
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-512.6
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-841.63
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-151.77
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Remitbee,-387.39
Chequing,20002-17758460,3/31/2025,,e-Transfer sent,Susan Lee,-300
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
Chequing,20002-17758460,3/31/2025,,INTERAC e-Transfer fee,,-1.5
1 Account Type Account Number Transaction Date Cheque Number Description 1 Description 2 Amount
2 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 2121.2
3 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 5376.41
4 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 1112.29
5 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 918.99
6 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 20559.97
7 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 1772.18
8 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 416.09
9 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 6960.58
10 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 416.09
11 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 694.58
12 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 1669.29
13 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 5415.81
14 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 572.39
15 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 2149.83
16 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 3583.87
17 Chequing 20002-17758460 1/2/2025 Misc Payment MK INC 2783.25
18 Chequing 20002-17758460 1/2/2025 Online transfer sent - 2155 Findlay Wong -3872.71
19 Chequing 20002-17758460 1/2/2025 Regular transaction fee 1 Dr @ 2.50 -2.5
20 Chequing 20002-17758460 1/2/2025 Monthly fee -6
21 Chequing 20002-17758460 1/2/2025 Bill Payment PAY-FILE FEES -4
22 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -192.77
23 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -188.77
24 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -270.94
25 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -446
26 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -314.1
27 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -245.93
28 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -224.49
29 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -287.98
30 Chequing 20002-17758460 1/6/2025 Online Banking transfer - 9316 -1413.24
31 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -192.77
32 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -560.99
33 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -366.99
34 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -237.99
35 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -132.7
36 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -392.1
37 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -696.66
38 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -288.34
39 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -661.49
40 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -389.99
41 Chequing 20002-17758460 1/6/2025 e-Transfer sent -431.71
42 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -176.8
43 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -196.99
44 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -997.35
45 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -418.05
46 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -1000
47 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -518.81
48 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -392.1
49 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -667.37
50 Chequing 20002-17758460 1/6/2025 e-Transfer sent Remitbee -236.46
51 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
52 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
53 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
54 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
55 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
56 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
57 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
58 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
59 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
60 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
61 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
62 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
63 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
64 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
65 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
66 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
67 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
68 Chequing 20002-17758460 1/6/2025 INTERAC e-Transfer fee -1.5
69 Chequing 20002-17758460 1/7/2025 e-Transfer sent Remitbee -6494.44
70 Chequing 20002-17758460 1/7/2025 INTERAC e-Transfer fee -1.5
71 Chequing 20002-17758460 1/8/2025 Insurance THE EMPIRE LIFE -2200.95
72 Chequing 20002-17758460 1/9/2025 Misc Payment MK INC 7164.85
73 Chequing 20002-17758460 1/9/2025 Misc Payment MK INC 851.05
74 Chequing 20002-17758460 1/9/2025 Misc Payment MK INC 15774.65
75 Chequing 20002-17758460 1/9/2025 COMMERCIAL TAXES EMPTX 8584774 -2577.54
76 Chequing 20002-17758460 1/9/2025 COMMERCIAL TAXES EMPTX 8584114 -2577.54
77 Chequing 20002-17758460 1/10/2025 Misc Payment MK INC 6711.7
78 Chequing 20002-17758460 1/10/2025 ATM deposit - TY916139 2252.25
79 Chequing 20002-17758460 1/10/2025 ATM/Mobile adjustment credit 916.67
80 Chequing 20002-17758460 1/14/2025 Misc Payment STRIPE 4363.08
81 Chequing 20002-17758460 1/16/2025 e-Transfer sent Remitbee -71.59
82 Chequing 20002-17758460 1/16/2025 INTERAC e-Transfer fee -1.5
83 Chequing 20002-17758460 1/16/2025 COMMERCIAL TAXES EMPTX 5448200 -414.5
84 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -716.08
85 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -191.1
86 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -311.36
87 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -246.46
88 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -156.3
89 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -224.98
90 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -644.47
91 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -182.81
92 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -189.17
93 Chequing 20002-17758460 1/20/2025 e-Transfer sent -191.1
94 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -432.64
95 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -999.53
96 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -418.75
97 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -446.96
98 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -132.91
99 Chequing 20002-17758460 1/20/2025 ATM deposit - TY916817 640
100 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -519.7
101 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -234.99
102 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -271.52
103 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -668.82
104 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -371.99
105 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -236.85
106 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -392.76
107 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -392.76
108 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -288.82
109 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -198.99
110 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -1000
111 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -567.99
112 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -698.18
113 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -240.99
114 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -662.61
115 Chequing 20002-17758460 1/20/2025 e-Transfer sent Remitbee -672.95
116 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
117 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
118 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
119 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
120 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
121 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
122 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
123 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
124 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
125 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
126 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
127 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
128 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
129 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
130 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
131 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
132 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
133 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
134 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
135 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
136 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
137 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
138 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
139 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
140 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
141 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
142 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
143 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
144 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
145 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
146 Chequing 20002-17758460 1/20/2025 INTERAC e-Transfer fee -1.5
147 Chequing 20002-17758460 1/20/2025 Misc Payment STRIPE -12.77
148 Chequing 20002-17758460 1/21/2025 Misc Payment STRIPE 2194.16
149 Chequing 20002-17758460 1/22/2025 e-Transfer sent Ajai Srivastava -565
150 Chequing 20002-17758460 1/22/2025 INTERAC e-Transfer fee -1.5
151 Chequing 20002-17758460 1/23/2025 Misc Payment STRIPE -31.92
152 Chequing 20002-17758460 1/24/2025 Misc Payment CITY OF RICHMON 282.5
153 Chequing 20002-17758460 1/24/2025 Misc Payment STRIPE 5485.85
154 Chequing 20002-17758460 1/27/2025 e-Transfer sent Susan Lee -160
155 Chequing 20002-17758460 1/27/2025 Online Banking transfer - 3607 -41.79
156 Chequing 20002-17758460 1/27/2025 INTERAC e-Transfer fee -1.5
157 Chequing 20002-17758460 1/30/2025 e-Transfer sent -3872.71
158 Chequing 20002-17758460 1/30/2025 e-Transfer sent Sok Kuan Mark -2100
159 Chequing 20002-17758460 1/30/2025 e-Transfer sent Sok Kuan Mark -1200
160 Chequing 20002-17758460 1/30/2025 Online Banking transfer - 1072 -3.5
161 Chequing 20002-17758460 1/30/2025 INTERAC e-Transfer fee -1.5
162 Chequing 20002-17758460 1/30/2025 INTERAC e-Transfer fee -1.5
163 Chequing 20002-17758460 1/30/2025 INTERAC e-Transfer fee -1.5
164 Chequing 20002-17758460 1/31/2025 Online transfer sent - 5947 ryan wong -3872.71
165 Chequing 20002-17758460 1/31/2025 Online Banking transfer - 5870 -1649.19
166 Chequing 20002-17758460 1/31/2025 Misc Payment STRIPE 59.56
167 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -336.32
168 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -451.56
169 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -283.35
170 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -698.28
171 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -672.86
172 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -1046.67
173 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -519.28
174 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -466.51
175 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -728.93
176 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -206.32
177 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -257.18
178 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -747.62
179 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -206.32
180 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -283.35
181 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -234.75
182 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -290.3
183 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -522.43
184 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -133.58
185 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -373.99
186 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -200.99
187 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -133.58
188 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -154.48
189 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -394.77
190 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -571.99
191 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -420.89
192 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -394.77
193 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -242.99
194 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -1000
195 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -395.07
196 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -389.99
197 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -666.51
198 Chequing 20002-17758460 2/3/2025 e-Transfer sent Remitbee -674.5
199 Chequing 20002-17758460 2/3/2025 Monthly fee -6
200 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
201 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
202 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
203 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
204 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
205 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
206 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
207 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
208 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
209 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
210 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
211 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
212 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
213 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
214 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
215 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
216 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
217 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
218 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
219 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
220 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
221 Chequing 20002-17758460 2/3/2025 INTERAC e-Transfer fee -1.5
222 Chequing 20002-17758460 2/3/2025 Bill Payment PAY-FILE FEES -6
223 Chequing 20002-17758460 2/5/2025 Misc Payment STRIPE 122.37
224 Chequing 20002-17758460 2/6/2025 Misc Payment STRIPE 62.81
225 Chequing 20002-17758460 2/7/2025 e-Transfer sent Remitbee -173.94
226 Chequing 20002-17758460 2/7/2025 e-Transfer received ANDREA IVANKA INTERNATIONAL IN 120
227 Chequing 20002-17758460 2/7/2025 Online Banking transfer - 1371 -0.04
228 Chequing 20002-17758460 2/7/2025 INTERAC e-Transfer fee -1.5
229 Chequing 20002-17758460 2/7/2025 Misc Payment STRIPE 314.05
230 Chequing 20002-17758460 2/10/2025 Misc Payment STRIPE 179.03
231 Chequing 20002-17758460 2/10/2025 ATM deposit - TY901067 2252.25
232 Chequing 20002-17758460 2/10/2025 Insurance EMP LIFE -2200.95
233 Chequing 20002-17758460 2/10/2025 ATM/Mobile adjustment credit 898.2
234 Chequing 20002-17758460 2/11/2025 COMMERCIAL TAXES EMPTX 1134350 -2539.4
235 Chequing 20002-17758460 2/12/2025 Misc Payment STRIPE 125.62
236 Chequing 20002-17758460 2/13/2025 Misc Payment STRIPE 304.65
237 Chequing 20002-17758460 2/14/2025 Misc Payment STRIPE 314.05
238 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -330.09
239 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -269.42
240 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -202.52
241 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -269.42
242 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -223.24
243 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -710.47
244 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -639.43
245 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -156.35
246 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -202.52
247 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -244.55
248 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -429.28
249 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -692.71
250 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -663.58
251 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -443.48
252 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -559
253 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -196.99
254 Chequing 20002-17758460 2/17/2025 e-Transfer sent -130.34
255 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -366.99
256 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -389.99
257 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -237.99
258 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -994.67
259 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -130.35
260 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -385.06
261 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -823.72
262 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -649.49
263 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -385.04
264 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -308.64
265 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -283.17
266 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -560.32
267 Chequing 20002-17758460 2/17/2025 e-Transfer sent Remitbee -1000
268 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
269 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
270 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
271 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
272 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
273 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
274 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
275 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
276 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
277 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
278 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
279 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
280 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
281 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
282 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
283 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
284 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
285 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
286 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
287 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
288 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
289 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
290 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
291 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
292 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
293 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
294 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
295 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
296 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
297 Chequing 20002-17758460 2/17/2025 INTERAC e-Transfer fee -1.5
298 Chequing 20002-17758460 2/18/2025 Misc Payment STRIPE 125.62
299 Chequing 20002-17758460 2/18/2025 e-Transfer sent Ajai Srivastava -1412.5
300 Chequing 20002-17758460 2/18/2025 INTERAC e-Transfer fee -1.5
301 Chequing 20002-17758460 2/19/2025 Misc Payment STRIPE 188.43
302 Chequing 20002-17758460 2/20/2025 Misc Payment STRIPE 62.81
303 Chequing 20002-17758460 2/21/2025 Misc Payment STRIPE 358.06
304 Chequing 20002-17758460 2/24/2025 Misc Payment STRIPE 62.81
305 Chequing 20002-17758460 2/25/2025 Misc Payment STRIPE -30.32
306 Chequing 20002-17758460 2/26/2025 Misc Payment STRIPE 5397.47
307 Chequing 20002-17758460 2/27/2025 Misc Payment STRIPE 304.65
308 Chequing 20002-17758460 2/28/2025 Matured GIC 100000
309 Chequing 20002-17758460 2/28/2025 GIC interest 4500
310 Chequing 20002-17758460 2/28/2025 Misc Payment STRIPE 62.81
311 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -735.5
312 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -367.99
313 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -278.8
314 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -658.72
315 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -417.7
316 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -314.02
317 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -132.59
318 Chequing 20002-17758460 3/3/2025 e-Transfer sent -196.99
319 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -686.95
320 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -237.99
321 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -717.11
322 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -559
323 Chequing 20002-17758460 3/3/2025 Misc Payment STRIPE 1137.39
324 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -459
325 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -1024.68
326 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -444.29
327 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -1000
328 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -832.17
329 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -518.1
330 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -660.58
331 Chequing 20002-17758460 3/3/2025 Online transfer sent - 5372 ryan wong -3872.71
332 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -391.55
333 Chequing 20002-17758460 3/3/2025 e-Transfer sent Remitbee -391.57
334 Chequing 20002-17758460 3/3/2025 e-Transfer sent Susan Lee -160
335 Chequing 20002-17758460 3/3/2025 e-Transfer sent Sok Kuan Mark -2100
336 Chequing 20002-17758460 3/3/2025 e-Transfer sent Chuck Windley -3872.71
337 Chequing 20002-17758460 3/3/2025 e-Transfer sent Sok Kuan Mark -1200
338 Chequing 20002-17758460 3/3/2025 Online Banking transfer - 4576 -3035.8
339 Chequing 20002-17758460 3/3/2025 Online Banking transfer - 3265 -42.34
340 Chequing 20002-17758460 3/3/2025 Monthly fee -6
341 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
342 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
343 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
344 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
345 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
346 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
347 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
348 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
349 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
350 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
351 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
352 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
353 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
354 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
355 Chequing 20002-17758460 3/3/2025 INTERAC e-Transfer fee -1.5
356 Chequing 20002-17758460 3/3/2025 COMMERCIAL TAXES EMPTX 3123370 -2539.4
357 Chequing 20002-17758460 3/3/2025 Bill Payment PAY-FILE FEES -2
358 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -154.06
359 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -278.8
360 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -234.99
361 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -253.06
362 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -146.25
363 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -363.38
364 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -346.13
365 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -212.3
366 Chequing 20002-17758460 3/4/2025 e-Transfer sent Remitbee -212.3
367 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
368 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
369 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
370 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
371 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
372 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
373 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
374 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
375 Chequing 20002-17758460 3/4/2025 INTERAC e-Transfer fee -1.5
376 Chequing 20002-17758460 3/4/2025 Misc Payment STRIPE 2742.77
377 Chequing 20002-17758460 3/7/2025 e-Transfer sent Remitbee -71.3
378 Chequing 20002-17758460 3/7/2025 e-Transfer received AJAI KUMARSRIVASTAVA 1412.5
379 Chequing 20002-17758460 3/7/2025 INTERAC e-Transfer fee -1.5
380 Chequing 20002-17758460 3/10/2025 Misc Payment STRIPE 116.22
381 Chequing 20002-17758460 3/10/2025 Online Banking payment - 7743 VISA - BNS -224.76
382 Chequing 20002-17758460 3/10/2025 Online Banking transfer - 2890 -0.06
383 Chequing 20002-17758460 3/10/2025 Insurance EMP LIFE -2200.95
384 Chequing 20002-17758460 3/11/2025 Misc Payment STRIPE 62.81
385 Chequing 20002-17758460 3/12/2025 Misc Payment STRIPE 62.81
386 Chequing 20002-17758460 3/13/2025 Misc Payment STRIPE 62.81
387 Chequing 20002-17758460 3/14/2025 Misc Payment STRIPE 187.91
388 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -363.58
389 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -278.95
390 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -717.51
391 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -278.95
392 Chequing 20002-17758460 3/17/2025 e-Transfer sent -662.31
393 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -564
394 Chequing 20002-17758460 3/17/2025 Misc Payment STRIPE 376.86
395 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -253.2
396 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -687.33
397 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -198.99
398 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -333.18
399 Chequing 20002-17758460 3/17/2025 e-Transfer sent -459.25
400 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -444.53
401 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -370.99
402 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -239.99
403 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -1471.81
404 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -415.14
405 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -515.13
406 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -831.48
407 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -389.34
408 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -515.07
409 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -85.4
410 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -389.99
411 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -1969.39
412 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -131.76
413 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -1000
414 Chequing 20002-17758460 3/17/2025 e-Transfer sent Remitbee -131.79
415 Chequing 20002-17758460 3/17/2025 e-Transfer sent -312.03
416 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
417 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
418 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
419 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
420 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
421 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
422 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
423 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
424 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
425 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
426 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
427 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
428 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
429 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
430 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
431 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
432 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
433 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
434 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
435 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
436 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
437 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
438 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
439 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
440 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
441 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
442 Chequing 20002-17758460 3/17/2025 INTERAC e-Transfer fee -1.5
443 Chequing 20002-17758460 3/18/2025 e-Transfer sent -157
444 Chequing 20002-17758460 3/18/2025 e-Transfer sent Remitbee -204.41
445 Chequing 20002-17758460 3/18/2025 e-Transfer sent -204.41
446 Chequing 20002-17758460 3/18/2025 INTERAC e-Transfer fee -1.5
447 Chequing 20002-17758460 3/18/2025 INTERAC e-Transfer fee -1.5
448 Chequing 20002-17758460 3/18/2025 INTERAC e-Transfer fee -1.5
449 Chequing 20002-17758460 3/18/2025 Misc Payment STRIPE 430.27
450 Chequing 20002-17758460 3/19/2025 Misc Payment STRIPE 62.81
451 Chequing 20002-17758460 3/20/2025 Misc Payment STRIPE 669.92
452 Chequing 20002-17758460 3/21/2025 Misc Payment STRIPE 420.87
453 Chequing 20002-17758460 3/24/2025 Misc Payment STRIPE 188.43
454 Chequing 20002-17758460 3/25/2025 Misc Payment STRIPE 188.43
455 Chequing 20002-17758460 3/26/2025 Online Banking transfer - 8674 -42.73
456 Chequing 20002-17758460 3/26/2025 e-Transfer sent Sok Kuan Mark -2100
457 Chequing 20002-17758460 3/26/2025 e-Transfer sent Sok Kuan Mark -1200
458 Chequing 20002-17758460 3/26/2025 e-Transfer sent Susan Lee -160
459 Chequing 20002-17758460 3/26/2025 Online Banking transfer - 9723 -42.73
460 Chequing 20002-17758460 3/26/2025 Online Banking transfer - 1905 -4653.31
461 Chequing 20002-17758460 3/26/2025 INTERAC e-Transfer fee -1.5
462 Chequing 20002-17758460 3/26/2025 INTERAC e-Transfer fee -1.5
463 Chequing 20002-17758460 3/26/2025 INTERAC e-Transfer fee -1.5
464 Chequing 20002-17758460 3/27/2025 Misc Payment STRIPE 429.31
465 Chequing 20002-17758460 3/28/2025 Online transfer sent - 9210 ryan wong -3872.71
466 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 1711.73
467 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 7135.05
468 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 13225.25
469 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 16739.66
470 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 4460.31
471 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 1723.59
472 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 2844.67
473 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 2816.13
474 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 1661.4
475 Chequing 20002-17758460 3/28/2025 Misc Payment MK INC 1464.54
476 Chequing 20002-17758460 3/28/2025 e-Transfer sent Chuck Windley -3872.71
477 Chequing 20002-17758460 3/28/2025 INTERAC e-Transfer fee -1.5
478 Chequing 20002-17758460 3/28/2025 Misc Payment STRIPE 2826.11
479 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -703.67
480 Chequing 20002-17758460 3/31/2025 Misc Payment STARTUP SLANG I 1000
481 Chequing 20002-17758460 3/31/2025 Misc Payment STRIPE 4425.37
482 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -674.08
483 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -198.99
484 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -240.99
485 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -131.12
486 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -1025.07
487 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -1000
488 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -370.99
489 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -310.55
490 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -1025.07
491 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -653.48
492 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -649.54
493 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -1443.42
494 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -512.6
495 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -841.63
496 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -151.77
497 Chequing 20002-17758460 3/31/2025 e-Transfer sent Remitbee -387.39
498 Chequing 20002-17758460 3/31/2025 e-Transfer sent Susan Lee -300
499 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
500 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
501 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
502 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
503 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
504 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
505 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
506 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
507 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
508 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
509 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
510 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
511 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
512 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
513 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
514 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
515 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
516 Chequing 20002-17758460 3/31/2025 INTERAC e-Transfer fee -1.5
+9
View File
@@ -0,0 +1,9 @@
import os
from dotenv import load_dotenv
load_dotenv()
GROQ_API_KEY = "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"
CONFIDENCE_THRESHOLD = 0.3
DATE_TOLERANCE_DAYS = 7
AMOUNT_TOLERANCE_PERCENT = 0.05
+82
View File
@@ -0,0 +1,82 @@
import csv
from dateutil import parser
from datetime import datetime, timedelta
# Config values
DATE_TOLERANCE_DAYS = 7
AMOUNT_TOLERANCE_PERCENT = 0.05
CONFIDENCE_THRESHOLD = 0.8
# Receipt data
receipt_date = datetime(2025, 2, 7)
receipt_amount = 1412.5
receipt_vendor = "Ajai Srivastava CPA, Accounting Services & Taxes"
print("=== DEBUGGING AJAI RECEIPT MATCH ===")
print(f"Receipt Date: {receipt_date}")
print(f"Receipt Amount: ${receipt_amount}")
print(f"Receipt Vendor: {receipt_vendor}")
print(f"Date Tolerance: {DATE_TOLERANCE_DAYS} days")
print(f"Amount Tolerance: {AMOUNT_TOLERANCE_PERCENT * 100}%")
print()
# Check CSV transaction
csv_transaction = {
"date": "2/18/2025",
"amount": -1412.5,
"vendor": "Ajai Srivastava"
}
# Parse CSV date
csv_date = parser.parse(csv_transaction["date"])
csv_amount = csv_transaction["amount"]
csv_vendor = csv_transaction["vendor"]
print("=== CSV TRANSACTION ===")
print(f"CSV Date: {csv_date}")
print(f"CSV Amount: ${csv_amount}")
print(f"CSV Vendor: {csv_vendor}")
print()
# Check date tolerance
date_diff = abs((receipt_date - csv_date).days)
date_match = date_diff <= DATE_TOLERANCE_DAYS
print("=== DATE CHECK ===")
print(f"Date Difference: {date_diff} days")
print(f"Date Match: {date_match}")
print(f"Tolerance: {DATE_TOLERANCE_DAYS} days")
print()
# Check amount tolerance
amount_tolerance = receipt_amount * AMOUNT_TOLERANCE_PERCENT
amount_diff = abs(receipt_amount - abs(csv_amount)) # Use absolute value for negative amounts
amount_match = amount_diff <= amount_tolerance
print("=== AMOUNT CHECK ===")
print(f"Receipt Amount: ${receipt_amount}")
print(f"CSV Amount (abs): ${abs(csv_amount)}")
print(f"Amount Difference: ${amount_diff}")
print(f"Amount Tolerance: ${amount_tolerance}")
print(f"Amount Match: {amount_match}")
print()
# Check vendor similarity
vendor_similarity = "Ajai Srivastava" in receipt_vendor
print("=== VENDOR CHECK ===")
print(f"Receipt Vendor: {receipt_vendor}")
print(f"CSV Vendor: {csv_vendor}")
print(f"Vendor Similarity: {vendor_similarity}")
print()
# Overall result
print("=== RESULT ===")
if date_match and amount_match:
print("✅ Transaction would pass initial filtering")
print("Would proceed to AI matching stage")
else:
print("❌ Transaction filtered out before AI matching")
if not date_match:
print(f" - Date difference ({date_diff} days) > tolerance ({DATE_TOLERANCE_DAYS} days)")
if not amount_match:
print(f" - Amount difference (${amount_diff}) > tolerance (${amount_tolerance})")
+204
View File
@@ -0,0 +1,204 @@
import groq
import base64
import io
from PIL import Image
import PyPDF2
from typing import Dict, Any, List, Optional
import config
import os
import aiofiles
from datetime import datetime
class DocumentProcessor:
def __init__(self):
self.client = groq.Groq(api_key=config.GROQ_API_KEY)
self.model = "meta-llama/llama-4-scout-17b-16e-instruct" # Vision model
async def process_file(self, file_path: str, file_type: str) -> Dict[str, Any]:
"""Process uploaded file and extract receipt data"""
try:
if file_type.lower() in ['jpg', 'jpeg', 'png', 'gif', 'bmp']:
return await self._process_image(file_path)
elif file_type.lower() == 'pdf':
return await self._process_pdf(file_path)
else:
raise ValueError(f"Unsupported file type: {file_type}")
except Exception as e:
return {"error": str(e)}
async def _process_image(self, image_path: str) -> Dict[str, Any]:
"""Extract data from image using Groq vision"""
try:
# Encode image to base64
base64_image = self._encode_image(image_path)
# Create Groq vision prompt
prompt = """
Analyze this receipt image and extract the following information in JSON format:
{
"vendor": "Store/company name",
"total_amount": 0.00,
"tax_amount": 0.00,
"date": "YYYY-MM-DD",
"category": "Food/Transport/Office/Other",
"confidence": 0.95
}
Rules:
- Extract vendor name as it appears on receipt
- Total amount should be the final total including tax
- Tax amount is separate tax line if available
- Date should be the date on the receipt
- Categorize based on vendor type (Starbucks=Food, Shell=Transport, etc.)
- Confidence score 0-1 based on how clear the receipt is
Return only valid JSON.
"""
# Call Groq vision API with correct format
response = self.client.chat.completions.create(
messages=[
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}",
},
},
],
}
],
model=self.model,
max_tokens=500,
temperature=0.1
)
# Parse response
result_text = response.choices[0].message.content.strip()
return self._parse_extraction_result(result_text)
except Exception as e:
return {"error": f"Image processing error: {str(e)}"}
def _encode_image(self, image_path: str) -> str:
"""Encode image to base64 string"""
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read()).decode('utf-8')
async def _process_pdf(self, pdf_path: str) -> Dict[str, Any]:
"""Extract data from PDF by converting to image first"""
try:
# For now, extract text from PDF and process as text
text_content = self._extract_text_from_pdf(pdf_path)
return self._process_text_content(text_content)
except Exception as e:
return {"error": f"PDF processing error: {str(e)}"}
def _extract_text_from_pdf(self, pdf_path: str) -> str:
"""Extract text from PDF"""
try:
with open(pdf_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
text = ""
for page in pdf_reader.pages:
text += page.extract_text() + "\n"
return text
except Exception as e:
return ""
def _process_text_content(self, text_content: str) -> Dict[str, Any]:
"""Process text content using Groq (fallback for PDFs)"""
try:
prompt = f"""
Analyze this receipt text and extract the following information in JSON format:
Receipt Text:
{text_content}
Extract:
{{
"vendor": "Store/company name",
"total_amount": 0.00,
"tax_amount": 0.00,
"date": "YYYY-MM-DD",
"category": "Food/Transport/Office/Other",
"confidence": 0.95
}}
Rules:
- Extract vendor name as it appears on receipt
- Total amount should be the final total including tax
- Tax amount is separate tax line if available
- Date should be the date on the receipt
- Categorize based on vendor type
- Confidence score 0-1 based on clarity
Return only valid JSON.
"""
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
temperature=0.1
)
result_text = response.choices[0].message.content.strip()
return self._parse_extraction_result(result_text)
except Exception as e:
return {"error": f"Text processing error: {str(e)}"}
def _parse_extraction_result(self, result_text: str) -> Dict[str, Any]:
"""Parse Groq response and extract JSON data"""
try:
# Clean up response and extract JSON
import json
import re
# Find JSON in response
json_match = re.search(r'\{.*\}', result_text, re.DOTALL)
if json_match:
json_str = json_match.group()
data = json.loads(json_str)
# Validate and clean data
return {
"vendor": data.get("vendor", "").strip(),
"total_amount": float(data.get("total_amount", 0)),
"tax_amount": float(data.get("tax_amount", 0)),
"date": data.get("date", ""),
"category": data.get("category", "Other"),
"confidence": float(data.get("confidence", 0.5)),
"extraction_success": True
}
else:
return {"error": "Could not parse JSON from AI response"}
except Exception as e:
return {"error": f"JSON parsing error: {str(e)}"}
async def save_uploaded_file(self, file_content: bytes, filename: str) -> str:
"""Save uploaded file to temporary storage"""
try:
# Create uploads directory if it doesn't exist
upload_dir = "uploads"
os.makedirs(upload_dir, exist_ok=True)
# Generate unique filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = f"{timestamp}_{filename.replace(' ', '_')}"
file_path = os.path.join(upload_dir, safe_filename)
# Save file
async with aiofiles.open(file_path, 'wb') as f:
await f.write(file_content)
return file_path
except Exception as e:
raise Exception(f"File save error: {str(e)}")
+60
View File
@@ -0,0 +1,60 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import List, Optional
import json
import os
@dataclass
class FeedbackLog:
transaction_id: str
original_match: str
correction: str
reason: str
timestamp: datetime
user_id: str
class FeedbackLogger:
def __init__(self, log_file: str = "feedback_logs.json"):
self.log_file = log_file
self.logs: List[FeedbackLog] = self._load_logs()
def _load_logs(self) -> List[FeedbackLog]:
if not os.path.exists(self.log_file):
return []
try:
with open(self.log_file, 'r') as f:
data = json.load(f)
return [FeedbackLog(**log) for log in data]
except:
return []
def _save_logs(self):
with open(self.log_file, 'w') as f:
json.dump([{
'transaction_id': log.transaction_id,
'original_match': log.original_match,
'correction': log.correction,
'reason': log.reason,
'timestamp': log.timestamp.isoformat(),
'user_id': log.user_id
} for log in self.logs], f, indent=2)
def log_override(self, transaction_id: str, original_match: str, correction: str, reason: str, user_id: str):
log = FeedbackLog(
transaction_id=transaction_id,
original_match=original_match,
correction=correction,
reason=reason,
timestamp=datetime.now(),
user_id=user_id
)
self.logs.append(log)
self._save_logs()
def get_logs_by_transaction(self, transaction_id: str) -> List[FeedbackLog]:
return [log for log in self.logs if log.transaction_id == transaction_id]
def get_recent_logs(self, days: int = 30) -> List[FeedbackLog]:
cutoff = datetime.now() - timedelta(days=days)
return [log for log in self.logs if log.timestamp > cutoff]
+137
View File
@@ -0,0 +1,137 @@
import os
import io
from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta
class GoogleDriveSync:
def __init__(self):
self.service = None
self.processed_files = set()
def authenticate(self):
"""Authenticate with Google Drive API"""
try:
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/drive.readonly']
# Load existing credentials
if os.path.exists('token.json'):
self.creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# If no valid credentials available, let user log in
if not self.creds or not self.creds.valid:
if self.creds and self.creds.expired and self.creds.refresh_token:
self.creds.refresh(Request())
else:
if not os.path.exists('credentials.json'):
raise Exception("credentials.json not found. Please download from Google Cloud Console.")
flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
self.creds = flow.run_local_server(port=0)
# Save credentials for next run
with open('token.json', 'w') as token:
token.write(self.creds.to_json())
# Build the Drive service
self.service = build('drive', 'v3', credentials=self.creds)
return True
except Exception as e:
print(f"Authentication error: {e}")
return False
def list_folders(self) -> List[Dict[str, Any]]:
"""List all folders in Google Drive"""
if not self.service:
if not self.authenticate():
return []
try:
results = self.service.files().list(
q="mimeType='application/vnd.google-apps.folder'",
pageSize=100,
fields="nextPageToken, files(id, name, createdTime, modifiedTime)"
).execute()
return results.get('files', [])
except Exception as e:
print(f"Error listing folders: {e}")
return []
def get_folder_info(self, folder_id: str) -> Dict[str, Any]:
"""Get information about a Google Drive folder"""
if not self.service:
if not self.authenticate():
return {}
try:
folder = self.service.files().get(
fileId=folder_id,
fields="id, name, createdTime, modifiedTime"
).execute()
return folder
except Exception as e:
print(f"Error getting folder info: {e}")
return {}
async def process_drive_files(self, folder_id: str = None) -> List[Dict[str, Any]]:
"""Process all receipt files from Google Drive"""
if not self.service:
if not self.authenticate():
return []
results = []
try:
# File types to look for
file_types = ["'application/pdf'", "'image/jpeg'", "'image/png'", "'image/gif'", "'image/bmp'"]
mime_types = " or ".join(file_types)
# Build query
query = f"mimeType contains {mime_types}"
if folder_id:
query += f" and '{folder_id}' in parents"
# Add date filter (last 30 days)
thirty_days_ago = (datetime.now() - timedelta(days=30)).isoformat() + 'Z'
query += f" and modifiedTime > '{thirty_days_ago}'"
results_files = self.service.files().list(
q=query,
pageSize=100,
fields="nextPageToken, files(id, name, mimeType, modifiedTime, size)"
).execute()
files = results_files.get('files', [])
files = [file for file in files if file['id'] not in self.processed_files]
# For demo purposes, return mock results
for file in files[:3]: # Process first 3 files
mock_result = {
"file_id": file['id'],
"filename": file['name'],
"drive_modified": file['modifiedTime'],
"file_size": file.get('size', 0),
"extraction_success": True,
"vendor": "Demo Vendor",
"total_amount": 25.50,
"tax_amount": 2.04,
"date": "2024-01-15",
"category": "Food",
"confidence": 0.95
}
results.append(mock_result)
self.processed_files.add(file['id'])
except Exception as e:
print(f"Error processing Drive files: {e}")
return results
+515
View File
@@ -0,0 +1,515 @@
from fastapi import FastAPI, HTTPException, UploadFile, File
from fastapi.middleware.cors import CORSMiddleware
from datetime import datetime
from typing import List
import uuid
import csv
import io
from api_models import (
MatchingRequest, MatchingResponse, MatchResponse,
ApprovalRequest, RuleRequest, DocumentUploadResponse,
DocumentProcessResponse, DriveSyncRequest, DriveSyncResponse,
QuickBooksImportRequest, QuickBooksImportResponse, TransactionRequest
)
from models import Receipt, Transaction, Match
from matching_engine import MatchingEngine
from ai_rules import AIRule
from document_processor import DocumentProcessor
from google_drive_sync import GoogleDriveSync
app = FastAPI(
title="AI Bookkeeper - Data Science Engine",
description="AI-powered receipt-to-transaction matching engine. Receives QuickBooks data from backend and provides intelligent matching capabilities.",
version="1.0.0"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize DS Engine components
matching_engine = MatchingEngine()
document_processor = DocumentProcessor()
drive_sync = GoogleDriveSync()
# In-memory storage for uploaded files (in production, use a database)
uploaded_files = {}
@app.get("/")
async def root():
"""Health check endpoint"""
return {
"message": "AI Bookkeeper Data Science Engine is running",
"version": "1.0.0",
"status": "healthy"
}
# ============================================================================
# QUICKBOOKS DATA IMPORT ENDPOINTS
# ============================================================================
@app.post("/transactions/import/quickbooks", response_model=QuickBooksImportResponse)
async def import_quickbooks_transactions(request: QuickBooksImportRequest):
"""
Import and convert QuickBooks transactions to internal format.
This endpoint receives raw QuickBooks transaction data from the backend
and converts it to the internal format used by the AI matching engine.
"""
try:
converted_transactions = []
errors = []
for qb_txn in request.transactions:
try:
# Convert QuickBooks date format to datetime
txn_date = datetime.strptime(qb_txn.txn_date, "%Y-%m-%d")
# Convert to internal TransactionRequest format
converted_txn = TransactionRequest(
id=qb_txn.id,
transaction_date=txn_date,
amount=abs(qb_txn.amount), # Ensure positive amount
vendor=qb_txn.payee_name,
notes=qb_txn.memo or f"QuickBooks transaction from {qb_txn.account_name or 'unknown account'}"
)
converted_transactions.append(converted_txn)
except Exception as e:
errors.append(f"Error converting transaction {qb_txn.id}: {str(e)}")
return QuickBooksImportResponse(
imported_count=len(converted_transactions),
converted_transactions=converted_transactions,
errors=errors
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/transactions/import/csv", response_model=QuickBooksImportResponse)
async def import_quickbooks_transactions_csv(file: UploadFile = File(...)):
"""
Import QuickBooks transactions from a CSV file (custom bank export format).
"""
try:
content = await file.read()
decoded = content.decode('utf-8')
reader = csv.DictReader(io.StringIO(decoded))
transactions = []
errors = []
for idx, row in enumerate(reader):
try:
# Use correct headers and strip whitespace
account_number = row.get('Account Number') or row.get('Account Number '.strip())
txn_date_raw = row.get('Transaction Date') or row.get('Transaction Date '.strip())
amount_raw = row.get('Amount') or row.get('Amount '.strip())
payee_name = row.get('Description 2') or row.get('Description 2 '.strip())
memo = f"{row.get('Account Type','').strip()} {row.get('Cheque Number','').strip()} {row.get('Description 1','').strip()}".strip()
# Compose ID
txn_id = f"{account_number}_{idx+1}"
# Parse date (try multiple formats)
txn_date_str = txn_date_raw.strip()
txn_date = None
for fmt in ("%m/%d/%y", "%m/%d/%Y"):
try:
txn_date = datetime.strptime(txn_date_str, fmt).strftime("%Y-%m-%d")
break
except Exception:
continue
if not txn_date:
raise ValueError(f"Could not parse date: {txn_date_str}")
# Parse amount
amount = float(amount_raw.replace(',', '').strip())
transactions.append({
"id": txn_id,
"txn_date": txn_date,
"amount": amount,
"payee_name": payee_name.strip(),
"memo": memo
})
except Exception as e:
errors.append(f"Row {idx+1}: {str(e)}")
# Use the same logic as the JSON import endpoint
request_obj = QuickBooksImportRequest(transactions=transactions)
response = await import_quickbooks_transactions(request_obj)
# Attach errors from CSV parsing
if hasattr(response, 'errors'):
response.errors.extend(errors)
return response
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# RECEIPT PROCESSING ENDPOINTS
# ============================================================================
@app.post("/upload", response_model=DocumentUploadResponse)
async def upload_document(file: UploadFile = File(...)):
"""
Upload a receipt document (PDF or image) for processing.
Supports: PDF, JPG, JPEG, PNG, GIF, BMP
"""
try:
# Validate file type
allowed_types = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp']
file_extension = file.filename.split('.')[-1].lower()
if file_extension not in allowed_types:
raise HTTPException(status_code=400, detail=f"Unsupported file type. Allowed: {allowed_types}")
# Read file content
file_content = await file.read()
# Save file
file_path = await document_processor.save_uploaded_file(file_content, file.filename)
# Generate file ID
file_id = str(uuid.uuid4())
# Store file info
uploaded_files[file_id] = {
"filename": file.filename,
"file_path": file_path,
"file_type": file_extension,
"upload_date": datetime.now(),
"status": "uploaded"
}
return DocumentUploadResponse(
file_id=file_id,
filename=file.filename,
file_type=file_extension,
upload_date=datetime.now(),
status="uploaded"
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/process/{file_id}", response_model=DocumentProcessResponse)
async def process_document(file_id: str):
"""
Process uploaded document and extract receipt data using AI.
Uses Groq LLM to extract vendor, amount, date, category from receipt images/PDFs.
"""
try:
if file_id not in uploaded_files:
raise HTTPException(status_code=404, detail="File not found")
file_info = uploaded_files[file_id]
file_path = file_info["file_path"]
file_type = file_info["file_type"]
# Process document using AI
result = await document_processor.process_file(file_path, file_type)
# Update file status
if "error" in result:
uploaded_files[file_id]["status"] = "failed"
else:
uploaded_files[file_id]["status"] = "processed"
uploaded_files[file_id]["extracted_data"] = result
return DocumentProcessResponse(
file_id=file_id,
extraction_success=result.get("extraction_success", False),
vendor=result.get("vendor"),
total_amount=result.get("total_amount"),
tax_amount=result.get("tax_amount"),
date=result.get("date"),
category=result.get("category"),
confidence=result.get("confidence"),
error=result.get("error")
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/documents")
async def list_documents():
"""List all uploaded and processed documents"""
try:
documents = []
for file_id, file_info in uploaded_files.items():
documents.append({
"file_id": file_id,
"filename": file_info["filename"],
"file_type": file_info["file_type"],
"upload_date": file_info["upload_date"],
"status": file_info["status"],
"extracted_data": file_info.get("extracted_data")
})
return {"documents": documents}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/upload-multiple", response_model=List[DocumentUploadResponse])
async def upload_multiple_documents(files: List[UploadFile] = File(...)):
"""
Upload multiple receipt documents (PDF or image) for processing.
Supports: PDF, JPG, JPEG, PNG, GIF, BMP
"""
responses = []
allowed_types = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp']
for file in files:
try:
file_extension = file.filename.split('.')[-1].lower()
if file_extension not in allowed_types:
responses.append(DocumentUploadResponse(
file_id="",
filename=file.filename,
file_type=file_extension,
upload_date=datetime.now(),
status=f"failed: unsupported file type ({file_extension})"
))
continue
file_content = await file.read()
file_path = await document_processor.save_uploaded_file(file_content, file.filename)
file_id = str(uuid.uuid4())
uploaded_files[file_id] = {
"filename": file.filename,
"file_path": file_path,
"file_type": file_extension,
"upload_date": datetime.now(),
"status": "uploaded"
}
responses.append(DocumentUploadResponse(
file_id=file_id,
filename=file.filename,
file_type=file_extension,
upload_date=datetime.now(),
status="uploaded"
))
except Exception as e:
responses.append(DocumentUploadResponse(
file_id="",
filename=file.filename,
file_type="",
upload_date=datetime.now(),
status=f"failed: {str(e)}"
))
return responses
# ============================================================================
# GOOGLE DRIVE INTEGRATION ENDPOINTS
# ============================================================================
@app.post("/drive/sync", response_model=DriveSyncResponse)
async def sync_google_drive(request: DriveSyncRequest):
"""
Sync and process receipts from Google Drive folder.
Automatically downloads and processes all receipt files from the specified
Google Drive folder using AI extraction.
"""
try:
# Process files from Drive
results = await drive_sync.process_drive_files(request.folder_id)
# Count results
files_processed = len(results)
successful_extractions = len([r for r in results if r.get("extraction_success", False)])
failed_extractions = files_processed - successful_extractions
# Convert to response format
response_results = []
for result in results:
response_results.append(DocumentProcessResponse(
file_id=result.get("file_id", ""),
extraction_success=result.get("extraction_success", False),
vendor=result.get("vendor"),
total_amount=result.get("total_amount"),
tax_amount=result.get("tax_amount"),
date=result.get("date"),
category=result.get("category"),
confidence=result.get("confidence"),
error=result.get("error")
))
return DriveSyncResponse(
files_processed=files_processed,
successful_extractions=successful_extractions,
failed_extractions=failed_extractions,
results=response_results
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/drive/folders")
async def list_drive_folders():
"""List all accessible Google Drive folders"""
try:
folders = drive_sync.list_folders()
return {"folders": folders}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/drive/folder/{folder_id}")
async def get_folder_info(folder_id: str):
"""Get information about a specific Google Drive folder"""
try:
folder_info = drive_sync.get_folder_info(folder_id)
return folder_info
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# AI MATCHING ENGINE ENDPOINTS
# ============================================================================
@app.post("/match", response_model=MatchingResponse)
async def match_receipts_transactions(request: MatchingRequest):
"""
Match receipts to transactions using AI.
Core AI matching engine that compares receipts against QuickBooks transactions
using intelligent algorithms and returns confidence scores.
"""
try:
# Convert request models to internal models
receipts = [
Receipt(
id=r.id, file_name=r.file_name, upload_date=r.upload_date,
receipt_date=r.receipt_date, amount=r.amount, tax=r.tax,
vendor=r.vendor, category=r.category
) for r in request.receipts
]
transactions = [
Transaction(
id=t.id, transaction_date=t.transaction_date, amount=t.amount,
vendor=t.vendor, notes=t.notes
) for t in request.transactions
]
# Process matching using AI engine
matches = matching_engine.process_matching(receipts, transactions)
# Convert to response format
match_responses = [
MatchResponse(
receipt_id=match.receipt.id,
transaction_id=match.transaction.id,
confidence_score=match.confidence_score,
match_reason=match.match_reason,
receipt_vendor=match.receipt.vendor,
receipt_amount=match.receipt.amount,
transaction_vendor=match.transaction.vendor,
transaction_amount=match.transaction.amount
) for match in matches
]
# Get statistics
stats = matching_engine.get_matching_stats(matches)
return MatchingResponse(matches=match_responses, stats=stats)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/approve")
async def approve_match(request: ApprovalRequest):
"""
Approve or reject an AI match.
Logs user feedback for continuous AI improvement and learning.
"""
try:
if request.action == "approve":
return {"message": f"Match {request.match_id} approved by {request.user_id}"}
elif request.action == "reject":
return {"message": f"Match {request.match_id} rejected by {request.user_id}. Reason: {request.reason}"}
else:
raise HTTPException(status_code=400, detail="Action must be 'approve' or 'reject'")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# AI RULES MANAGEMENT ENDPOINTS
# ============================================================================
@app.post("/rules")
async def add_rule(request: RuleRequest):
"""Add a new AI rule for matching and categorization"""
try:
rule = AIRule(
name=request.name,
condition=request.condition,
action=request.action,
source=request.source
)
matching_engine.rules_engine.add_rule(rule)
return {"message": f"Rule '{request.name}' added successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/rules")
async def get_rules():
"""Get all active AI rules"""
try:
rules = matching_engine.rules_engine.rules
return {
"rules": [
{
"name": rule.name,
"condition": rule.condition,
"action": rule.action,
"source": rule.source,
"status": rule.status
} for rule in rules
]
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.delete("/rules/{rule_name}")
async def delete_rule(rule_name: str):
"""Delete an AI rule"""
try:
matching_engine.rules_engine.remove_rule(rule_name)
return {"message": f"Rule '{rule_name}' deleted successfully"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============================================================================
# SYSTEM MONITORING ENDPOINTS
# ============================================================================
@app.get("/stats")
async def get_stats():
"""Get system statistics and performance metrics"""
try:
recent_logs = matching_engine.feedback_logger.get_recent_logs(30)
return {
"total_feedback_logs": len(matching_engine.feedback_logger.logs),
"recent_feedback_logs": len(recent_logs),
"active_rules": len([r for r in matching_engine.rules_engine.rules if r.status == "active"]),
"uploaded_documents": len(uploaded_files),
"processed_documents": len([f for f in uploaded_files.values() if f["status"] == "processed"])
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8343)
+73
View File
@@ -0,0 +1,73 @@
from typing import List, Dict, Any
from datetime import datetime
from ai_matcher import AIMatcher
from ai_rules import AIRulesEngine
from feedback_logger import FeedbackLogger
from models import Receipt, Transaction, Match
class MatchingEngine:
def __init__(self):
self.ai_matcher = AIMatcher()
self.rules_engine = AIRulesEngine()
self.feedback_logger = FeedbackLogger()
def process_matching(self, receipts: List[Receipt], transactions: List[Transaction]) -> List[Match]:
# Get AI matches
ai_matches = self.ai_matcher.match_receipts_to_transactions(receipts, transactions)
# Apply rules and enhance matches
enhanced_matches = []
for match in ai_matches:
enhanced_match = self._enhance_match_with_rules(match)
enhanced_matches.append(enhanced_match)
return enhanced_matches
def _enhance_match_with_rules(self, match: Match) -> Match:
rule_results = self.rules_engine.apply_rules(match.receipt, match.transaction)
# Apply confidence boost from rules
if rule_results["confidence_boost"] > 0:
match.confidence_score = min(1.0, match.confidence_score + rule_results["confidence_boost"])
# Auto-approve if rules say so
if rule_results["auto_approve"]:
match.confidence_score = 1.0
match.match_reason += " (Auto-approved by rules)"
return match
def approve_match(self, match: Match, user_id: str):
# Log the approval
self.feedback_logger.log_override(
transaction_id=match.transaction.id,
original_match=f"AI Score: {match.confidence_score}",
correction="Approved",
reason="User approved match",
user_id=user_id
)
def reject_match(self, match: Match, reason: str, user_id: str):
# Log the rejection
self.feedback_logger.log_override(
transaction_id=match.transaction.id,
original_match=f"AI Score: {match.confidence_score}",
correction="Rejected",
reason=reason,
user_id=user_id
)
def get_matching_stats(self, matches: List[Match]) -> Dict[str, Any]:
if not matches:
return {"total": 0, "high_confidence": 0, "low_confidence": 0, "avg_score": 0}
high_confidence = len([m for m in matches if m.confidence_score >= 0.8])
low_confidence = len([m for m in matches if m.confidence_score < 0.8])
avg_score = sum(m.confidence_score for m in matches) / len(matches)
return {
"total": len(matches),
"high_confidence": high_confidence,
"low_confidence": low_confidence,
"avg_score": round(avg_score, 3)
}
+29
View File
@@ -0,0 +1,29 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
@dataclass
class Receipt:
id: str
file_name: str
upload_date: datetime
receipt_date: datetime
amount: float
tax: float
vendor: str
category: str
@dataclass
class Transaction:
id: str
transaction_date: datetime
amount: float
vendor: str
notes: str
@dataclass
class Match:
receipt: Receipt
transaction: Transaction
confidence_score: float
match_reason: str
+16
View File
@@ -0,0 +1,16 @@
groq>=0.5.0
python-dotenv==1.0.0
pandas==2.1.4
numpy==1.24.3
fastapi==0.104.1
uvicorn==0.24.0
pydantic==2.5.0
requests==2.31.0
python-multipart==0.0.6
Pillow==10.0.1
PyPDF2==3.0.1
aiofiles==23.2.1
google-auth==2.23.4
google-auth-oauthlib==1.1.0
google-auth-httplib2==0.1.1
google-api-python-client==2.108.0
+49
View File
@@ -0,0 +1,49 @@
import json
import requests
import csv
from dateutil import parser
# Prepare transactions
transactions = []
with open("chequing statement.csv", newline="") as f:
reader = csv.DictReader(f)
idx = 1
for row in reader:
try:
txn_id = f"{row['Account Number']}_{idx}"
txn_date = parser.parse(row["Transaction Date"]).isoformat()
amount = float(row["Amount"].replace(",", "").strip())
vendor = row["Description 2"].strip()
notes = f"{row['Account Type']} {row['Cheque Number']} {row['Description 1']}".strip()
transactions.append({
"id": txn_id,
"transaction_date": txn_date,
"amount": amount,
"vendor": vendor,
"notes": notes
})
idx += 1
except Exception as e:
continue
# Receipt data for Ajai Invoice (3).jpg
receipt = {
"id": "33754868-bff5-4caf-9ece-cfd63f4e52d9",
"file_name": "Ajai Invoice (3).jpg",
"upload_date": "2025-07-02T15:31:23.641315",
"receipt_date": "2025-02-07T00:00:00",
"amount": 1412.5,
"tax": 162.5,
"vendor": "Ajai Srivastava CPA, Accounting Services & Taxes",
"category": "Office"
}
# Build request
data = {
"receipts": [receipt],
"transactions": transactions
}
# Post to /match
response = requests.post("http://localhost:8000/match", json=data)
print(json.dumps(response.json(), indent=2))
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+247
View File
@@ -0,0 +1,247 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"
+69
View File
@@ -0,0 +1,69 @@
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r 2> /dev/null
fi
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
VIRTUAL_ENV="/Users/user/mkd/quickbooks/venv"
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1="(venv) ${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT="(venv) "
export VIRTUAL_ENV_PROMPT
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r 2> /dev/null
fi
+26
View File
@@ -0,0 +1,26 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV "/Users/user/mkd/quickbooks/venv"
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = "(venv) $prompt"
setenv VIRTUAL_ENV_PROMPT "(venv) "
endif
alias pydoc python -m pydoc
rehash
+69
View File
@@ -0,0 +1,69 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/); you cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV "/Users/user/mkd/quickbooks/venv"
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) "(venv) " (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT "(venv) "
end
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from distro.distro import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy.f2py.f2py2e import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy.f2py.f2py2e import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from numpy.f2py.f2py2e import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from fastapi.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from google_auth_oauthlib.tool.__main__ import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from httpx import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from charset_normalizer import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli.cli_detect())
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import decrypt
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(decrypt())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import encrypt
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(encrypt())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import keygen
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(keygen())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from rsa.util import private_to_public
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(private_to_public())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import sign
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(sign())
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from rsa.cli import verify
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(verify())
+1
View File
@@ -0,0 +1 @@
python3
+1
View File
@@ -0,0 +1 @@
/Users/user/anaconda3/bin/python3
+1
View File
@@ -0,0 +1 @@
python3
+8
View File
@@ -0,0 +1,8 @@
#!/Users/user/mkd/quickbooks/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from uvicorn.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,292 @@
from __future__ import annotations
import os
from io import BytesIO
from typing import IO
from . import ExifTags, Image, ImageFile
try:
from . import _avif
SUPPORTED = True
except ImportError:
SUPPORTED = False
# Decoder options as module globals, until there is a way to pass parameters
# to Image.open (see https://github.com/python-pillow/Pillow/issues/569)
DECODE_CODEC_CHOICE = "auto"
# Decoding is only affected by this for libavif **0.8.4** or greater.
DEFAULT_MAX_THREADS = 0
def get_codec_version(codec_name: str) -> str | None:
versions = _avif.codec_versions()
for version in versions.split(", "):
if version.split(" [")[0] == codec_name:
return version.split(":")[-1].split(" ")[0]
return None
def _accept(prefix: bytes) -> bool | str:
if prefix[4:8] != b"ftyp":
return False
major_brand = prefix[8:12]
if major_brand in (
# coding brands
b"avif",
b"avis",
# We accept files with AVIF container brands; we can't yet know if
# the ftyp box has the correct compatible brands, but if it doesn't
# then the plugin will raise a SyntaxError which Pillow will catch
# before moving on to the next plugin that accepts the file.
#
# Also, because this file might not actually be an AVIF file, we
# don't raise an error if AVIF support isn't properly compiled.
b"mif1",
b"msf1",
):
if not SUPPORTED:
return (
"image file could not be identified because AVIF support not installed"
)
return True
return False
def _get_default_max_threads() -> int:
if DEFAULT_MAX_THREADS:
return DEFAULT_MAX_THREADS
if hasattr(os, "sched_getaffinity"):
return len(os.sched_getaffinity(0))
else:
return os.cpu_count() or 1
class AvifImageFile(ImageFile.ImageFile):
format = "AVIF"
format_description = "AVIF image"
__frame = -1
def _open(self) -> None:
if not SUPPORTED:
msg = "image file could not be opened because AVIF support not installed"
raise SyntaxError(msg)
if DECODE_CODEC_CHOICE != "auto" and not _avif.decoder_codec_available(
DECODE_CODEC_CHOICE
):
msg = "Invalid opening codec"
raise ValueError(msg)
self._decoder = _avif.AvifDecoder(
self.fp.read(),
DECODE_CODEC_CHOICE,
_get_default_max_threads(),
)
# Get info from decoder
self._size, self.n_frames, self._mode, icc, exif, exif_orientation, xmp = (
self._decoder.get_info()
)
self.is_animated = self.n_frames > 1
if icc:
self.info["icc_profile"] = icc
if xmp:
self.info["xmp"] = xmp
if exif_orientation != 1 or exif:
exif_data = Image.Exif()
if exif:
exif_data.load(exif)
original_orientation = exif_data.get(ExifTags.Base.Orientation, 1)
else:
original_orientation = 1
if exif_orientation != original_orientation:
exif_data[ExifTags.Base.Orientation] = exif_orientation
exif = exif_data.tobytes()
if exif:
self.info["exif"] = exif
self.seek(0)
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
# Set tile
self.__frame = frame
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.mode)]
def load(self) -> Image.core.PixelAccess | None:
if self.tile:
# We need to load the image data for this frame
data, timescale, pts_in_timescales, duration_in_timescales = (
self._decoder.get_frame(self.__frame)
)
self.info["timestamp"] = round(1000 * (pts_in_timescales / timescale))
self.info["duration"] = round(1000 * (duration_in_timescales / timescale))
if self.fp and self._exclusive_fp:
self.fp.close()
self.fp = BytesIO(data)
return super().load()
def load_seek(self, pos: int) -> None:
pass
def tell(self) -> int:
return self.__frame
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True)
def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None:
info = im.encoderinfo.copy()
if save_all:
append_images = list(info.get("append_images", []))
else:
append_images = []
total = 0
for ims in [im] + append_images:
total += getattr(ims, "n_frames", 1)
quality = info.get("quality", 75)
if not isinstance(quality, int) or quality < 0 or quality > 100:
msg = "Invalid quality setting"
raise ValueError(msg)
duration = info.get("duration", 0)
subsampling = info.get("subsampling", "4:2:0")
speed = info.get("speed", 6)
max_threads = info.get("max_threads", _get_default_max_threads())
codec = info.get("codec", "auto")
if codec != "auto" and not _avif.encoder_codec_available(codec):
msg = "Invalid saving codec"
raise ValueError(msg)
range_ = info.get("range", "full")
tile_rows_log2 = info.get("tile_rows", 0)
tile_cols_log2 = info.get("tile_cols", 0)
alpha_premultiplied = bool(info.get("alpha_premultiplied", False))
autotiling = bool(info.get("autotiling", tile_rows_log2 == tile_cols_log2 == 0))
icc_profile = info.get("icc_profile", im.info.get("icc_profile"))
exif_orientation = 1
if exif := info.get("exif"):
if isinstance(exif, Image.Exif):
exif_data = exif
else:
exif_data = Image.Exif()
exif_data.load(exif)
if ExifTags.Base.Orientation in exif_data:
exif_orientation = exif_data.pop(ExifTags.Base.Orientation)
exif = exif_data.tobytes() if exif_data else b""
elif isinstance(exif, Image.Exif):
exif = exif_data.tobytes()
xmp = info.get("xmp")
if isinstance(xmp, str):
xmp = xmp.encode("utf-8")
advanced = info.get("advanced")
if advanced is not None:
if isinstance(advanced, dict):
advanced = advanced.items()
try:
advanced = tuple(advanced)
except TypeError:
invalid = True
else:
invalid = any(not isinstance(v, tuple) or len(v) != 2 for v in advanced)
if invalid:
msg = (
"advanced codec options must be a dict of key-value string "
"pairs or a series of key-value two-tuples"
)
raise ValueError(msg)
# Setup the AVIF encoder
enc = _avif.AvifEncoder(
im.size,
subsampling,
quality,
speed,
max_threads,
codec,
range_,
tile_rows_log2,
tile_cols_log2,
alpha_premultiplied,
autotiling,
icc_profile or b"",
exif or b"",
exif_orientation,
xmp or b"",
advanced,
)
# Add each frame
frame_idx = 0
frame_duration = 0
cur_idx = im.tell()
is_single_frame = total == 1
try:
for ims in [im] + append_images:
# Get number of frames in this image
nfr = getattr(ims, "n_frames", 1)
for idx in range(nfr):
ims.seek(idx)
# Make sure image mode is supported
frame = ims
rawmode = ims.mode
if ims.mode not in {"RGB", "RGBA"}:
rawmode = "RGBA" if ims.has_transparency_data else "RGB"
frame = ims.convert(rawmode)
# Update frame duration
if isinstance(duration, (list, tuple)):
frame_duration = duration[frame_idx]
else:
frame_duration = duration
# Append the frame to the animation encoder
enc.add(
frame.tobytes("raw", rawmode),
frame_duration,
frame.size,
rawmode,
is_single_frame,
)
# Update frame index
frame_idx += 1
if not save_all:
break
finally:
im.seek(cur_idx)
# Get the final output from the encoder
data = enc.finish()
if data is None:
msg = "cannot write file as AVIF (encoder returned None)"
raise OSError(msg)
fp.write(data)
Image.register_open(AvifImageFile.format, AvifImageFile, _accept)
if SUPPORTED:
Image.register_save(AvifImageFile.format, _save)
Image.register_save_all(AvifImageFile.format, _save_all)
Image.register_extensions(AvifImageFile.format, [".avif", ".avifs"])
Image.register_mime(AvifImageFile.format, "image/avif")
@@ -0,0 +1,122 @@
#
# The Python Imaging Library
# $Id$
#
# bitmap distribution font (bdf) file parser
#
# history:
# 1996-05-16 fl created (as bdf2pil)
# 1997-08-25 fl converted to FontFile driver
# 2001-05-25 fl removed bogus __init__ call
# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
# 2003-04-22 fl more robustification (from Graham Dumpleton)
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1997-2003 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
"""
Parse X Bitmap Distribution Format (BDF)
"""
from __future__ import annotations
from typing import BinaryIO
from . import FontFile, Image
def bdf_char(
f: BinaryIO,
) -> (
tuple[
str,
int,
tuple[tuple[int, int], tuple[int, int, int, int], tuple[int, int, int, int]],
Image.Image,
]
| None
):
# skip to STARTCHAR
while True:
s = f.readline()
if not s:
return None
if s.startswith(b"STARTCHAR"):
break
id = s[9:].strip().decode("ascii")
# load symbol properties
props = {}
while True:
s = f.readline()
if not s or s.startswith(b"BITMAP"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
# load bitmap
bitmap = bytearray()
while True:
s = f.readline()
if not s or s.startswith(b"ENDCHAR"):
break
bitmap += s[:-1]
# The word BBX
# followed by the width in x (BBw), height in y (BBh),
# and x and y displacement (BBxoff0, BByoff0)
# of the lower left corner from the origin of the character.
width, height, x_disp, y_disp = (int(p) for p in props["BBX"].split())
# The word DWIDTH
# followed by the width in x and y of the character in device pixels.
dwx, dwy = (int(p) for p in props["DWIDTH"].split())
bbox = (
(dwx, dwy),
(x_disp, -y_disp - height, width + x_disp, -y_disp),
(0, 0, width, height),
)
try:
im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
except ValueError:
# deal with zero-width characters
im = Image.new("1", (width, height))
return id, int(props["ENCODING"]), bbox, im
class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format."""
def __init__(self, fp: BinaryIO) -> None:
super().__init__()
s = fp.readline()
if not s.startswith(b"STARTFONT 2.1"):
msg = "not a valid BDF file"
raise SyntaxError(msg)
props = {}
comments = []
while True:
s = fp.readline()
if not s or s.startswith(b"ENDPROPERTIES"):
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
if s.find(b"LogicalFontDescription") < 0:
comments.append(s[i + 1 : -1].decode("ascii"))
while True:
c = bdf_char(fp)
if not c:
break
id, ch, (xy, dst, src), im = c
if 0 <= ch < len(self.glyph):
self.glyph[ch] = xy, dst, src, im
@@ -0,0 +1,497 @@
"""
Blizzard Mipmap Format (.blp)
Jerome Leclanche <jerome@leclan.ch>
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
BLP1 files, used mostly in Warcraft III, are not fully supported.
All types of BLP2 files used in World of Warcraft are supported.
The BLP file structure consists of a header, up to 16 mipmaps of the
texture
Texture sizes must be powers of two, though the two dimensions do
not have to be equal; 512x256 is valid, but 512x200 is not.
The first mipmap (mipmap #0) is the full size image; each subsequent
mipmap halves both dimensions. The final mipmap should be 1x1.
BLP files come in many different flavours:
* JPEG-compressed (type == 0) - only supported for BLP1.
* RAW images (type == 1, encoding == 1). Each mipmap is stored as an
array of 8-bit values, one per pixel, left to right, top to bottom.
Each value is an index to the palette.
* DXT-compressed (type == 1, encoding == 2):
- DXT1 compression is used if alpha_encoding == 0.
- An additional alpha bit is used if alpha_depth == 1.
- DXT3 compression is used if alpha_encoding == 1.
- DXT5 compression is used if alpha_encoding == 7.
"""
from __future__ import annotations
import abc
import os
import struct
from enum import IntEnum
from io import BytesIO
from typing import IO
from . import Image, ImageFile
class Format(IntEnum):
JPEG = 0
class Encoding(IntEnum):
UNCOMPRESSED = 1
DXT = 2
UNCOMPRESSED_RAW_BGRA = 3
class AlphaEncoding(IntEnum):
DXT1 = 0
DXT3 = 1
DXT5 = 7
def unpack_565(i: int) -> tuple[int, int, int]:
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
def decode_dxt1(
data: bytes, alpha: bool = False
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
blocks = len(data) // 8 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block_index in range(blocks):
# Decode next 8-byte block.
idx = block_index * 8
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
# Decode this block into 4x4 pixels
# Accumulate the results onto our 4 row accumulators
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
control = bits & 3
bits = bits >> 2
a = 0xFF
if control == 0:
r, g, b = r0, g0, b0
elif control == 1:
r, g, b = r1, g1, b1
elif control == 2:
if color0 > color1:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
else:
r = (r0 + r1) // 2
g = (g0 + g1) // 2
b = (b0 + b1) // 2
elif control == 3:
if color0 > color1:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
else:
r, g, b, a = 0, 0, 0, 0
if alpha:
ret[j].extend([r, g, b, a])
else:
ret[j].extend([r, g, b])
return ret
def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block_index in range(blocks):
idx = block_index * 16
block = data[idx : idx + 16]
# Decode next 16-byte block.
bits = struct.unpack_from("<8B", block)
color0, color1 = struct.unpack_from("<HH", block, 8)
(code,) = struct.unpack_from("<I", block, 12)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
for j in range(4):
high = False # Do we want the higher bits?
for i in range(4):
alphacode_index = (4 * j + i) // 2
a = bits[alphacode_index]
if high:
high = False
a >>= 4
else:
high = True
a &= 0xF
a *= 17 # We get a value between 0 and 15
color_code = (code >> 2 * (4 * j + i)) & 0x03
if color_code == 0:
r, g, b = r0, g0, b0
elif color_code == 1:
r, g, b = r1, g1, b1
elif color_code == 2:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
elif color_code == 3:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
ret[j].extend([r, g, b, a])
return ret
def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
"""
input: one "row" of data (i.e. will produce 4 * width pixels)
"""
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block_index in range(blocks):
idx = block_index * 16
block = data[idx : idx + 16]
# Decode next 16-byte block.
a0, a1 = struct.unpack_from("<BB", block)
bits = struct.unpack_from("<6B", block, 2)
alphacode1 = bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24)
alphacode2 = bits[0] | (bits[1] << 8)
color0, color1 = struct.unpack_from("<HH", block, 8)
(code,) = struct.unpack_from("<I", block, 12)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
alphacode_index = 3 * (4 * j + i)
if alphacode_index <= 12:
alphacode = (alphacode2 >> alphacode_index) & 0x07
elif alphacode_index == 15:
alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06)
else: # alphacode_index >= 18 and alphacode_index <= 45
alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07
if alphacode == 0:
a = a0
elif alphacode == 1:
a = a1
elif a0 > a1:
a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7
elif alphacode == 6:
a = 0
elif alphacode == 7:
a = 255
else:
a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5
color_code = (code >> 2 * (4 * j + i)) & 0x03
if color_code == 0:
r, g, b = r0, g0, b0
elif color_code == 1:
r, g, b = r1, g1, b1
elif color_code == 2:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
elif color_code == 3:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
ret[j].extend([r, g, b, a])
return ret
class BLPFormatError(NotImplementedError):
pass
def _accept(prefix: bytes) -> bool:
return prefix.startswith((b"BLP1", b"BLP2"))
class BlpImageFile(ImageFile.ImageFile):
"""
Blizzard Mipmap Format
"""
format = "BLP"
format_description = "Blizzard Mipmap Format"
def _open(self) -> None:
self.magic = self.fp.read(4)
if not _accept(self.magic):
msg = f"Bad BLP magic {repr(self.magic)}"
raise BLPFormatError(msg)
compression = struct.unpack("<i", self.fp.read(4))[0]
if self.magic == b"BLP1":
alpha = struct.unpack("<I", self.fp.read(4))[0] != 0
else:
encoding = struct.unpack("<b", self.fp.read(1))[0]
alpha = struct.unpack("<b", self.fp.read(1))[0] != 0
alpha_encoding = struct.unpack("<b", self.fp.read(1))[0]
self.fp.seek(1, os.SEEK_CUR) # mips
self._size = struct.unpack("<II", self.fp.read(8))
args: tuple[int, int, bool] | tuple[int, int, bool, int]
if self.magic == b"BLP1":
encoding = struct.unpack("<i", self.fp.read(4))[0]
self.fp.seek(4, os.SEEK_CUR) # subtype
args = (compression, encoding, alpha)
offset = 28
else:
args = (compression, encoding, alpha, alpha_encoding)
offset = 20
decoder = self.magic.decode()
self._mode = "RGBA" if alpha else "RGB"
self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, offset, args)]
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
try:
self._read_header()
self._load()
except struct.error as e:
msg = "Truncated BLP file"
raise OSError(msg) from e
return -1, 0
@abc.abstractmethod
def _load(self) -> None:
pass
def _read_header(self) -> None:
self._offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length: int) -> bytes:
assert self.fd is not None
return ImageFile._safe_read(self.fd, length)
def _read_palette(self) -> list[tuple[int, int, int, int]]:
ret = []
for i in range(256):
try:
b, g, r, a = struct.unpack("<4B", self._safe_read(4))
except struct.error:
break
ret.append((b, g, r, a))
return ret
def _read_bgra(
self, palette: list[tuple[int, int, int, int]], alpha: bool
) -> bytearray:
data = bytearray()
_data = BytesIO(self._safe_read(self._lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
d: tuple[int, ...] = (r, g, b)
if alpha:
d += (a,)
data.extend(d)
return data
class BLP1Decoder(_BLPBaseDecoder):
def _load(self) -> None:
self._compression, self._encoding, alpha = self.args
if self._compression == Format.JPEG:
self._decode_jpeg_stream()
elif self._compression == 1:
if self._encoding in (4, 5):
palette = self._read_palette()
data = self._read_bgra(palette, alpha)
self.set_as_raw(data)
else:
msg = f"Unsupported BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unsupported BLP compression {repr(self._encoding)}"
raise BLPFormatError(msg)
def _decode_jpeg_stream(self) -> None:
from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
assert self.fd is not None
self._safe_read(self._offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._lengths[0])
data = jpeg_header + data
image = JpegImageFile(BytesIO(data))
Image._decompression_bomb_check(image.size)
if image.mode == "CMYK":
args = image.tile[0].args
assert isinstance(args, tuple)
image.tile = [image.tile[0]._replace(args=(args[0], "CMYK"))]
self.set_as_raw(image.convert("RGB").tobytes(), "BGR")
class BLP2Decoder(_BLPBaseDecoder):
def _load(self) -> None:
self._compression, self._encoding, alpha, self._alpha_encoding = self.args
palette = self._read_palette()
assert self.fd is not None
self.fd.seek(self._offsets[0])
if self._compression == 1:
# Uncompressed or DirectX compression
if self._encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette, alpha)
elif self._encoding == Encoding.DXT:
data = bytearray()
if self._alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.state.xsize + 3) // 4 * 8
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt1(self._safe_read(linesize), alpha):
data += d
elif self._alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)):
data += d
elif self._alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.state.xsize + 3) // 4 * 16
for yb in range((self.state.ysize + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
msg = f"Unsupported alpha encoding {repr(self._alpha_encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unknown BLP encoding {repr(self._encoding)}"
raise BLPFormatError(msg)
else:
msg = f"Unknown BLP compression {repr(self._compression)}"
raise BLPFormatError(msg)
self.set_as_raw(data)
class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True
def _write_palette(self) -> bytes:
data = b""
assert self.im is not None
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
while len(data) < 256 * 4:
data += b"\x00" * 4
return data
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
assert self.im is not None
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
data += palette_data
for y in range(h):
for x in range(w):
data += struct.pack("<B", self.im.getpixel((x, y)))
return len(data), 0, data
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "P":
msg = "Unsupported BLP image mode"
raise ValueError(msg)
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
assert im.palette is not None
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
alpha_depth = 1 if im.palette.mode == "RGBA" else 0
if magic == b"BLP1":
fp.write(struct.pack("<L", alpha_depth))
else:
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", alpha_depth))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
Image.register_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder)
Image.register_decoder("BLP2", BLP2Decoder)
Image.register_save(BlpImageFile.format, _save)
Image.register_encoder("BLP", BLPEncoder)
@@ -0,0 +1,515 @@
#
# The Python Imaging Library.
# $Id$
#
# BMP file handler
#
# Windows (and OS/2) native bitmap storage format.
#
# history:
# 1995-09-01 fl Created
# 1996-04-30 fl Added save
# 1997-08-27 fl Fixed save of 1-bit images
# 1998-03-06 fl Load P images as L where possible
# 1998-07-03 fl Load P images as 1 where possible
# 1998-12-29 fl Handle small palettes
# 2002-12-30 fl Fixed load of 1-bit palette images
# 2003-04-21 fl Fixed load of 1-bit monochrome images
# 2003-04-23 fl Added limited support for BI_BITFIELDS compression
#
# Copyright (c) 1997-2003 by Secret Labs AB
# Copyright (c) 1995-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations
import os
from typing import IO, Any
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
from ._binary import o16le as o16
from ._binary import o32le as o32
#
# --------------------------------------------------------------------
# Read BMP file
BIT2MODE = {
# bits => mode, rawmode
1: ("P", "P;1"),
4: ("P", "P;4"),
8: ("P", "P"),
16: ("RGB", "BGR;15"),
24: ("RGB", "BGR"),
32: ("RGB", "BGRX"),
}
USE_RAW_ALPHA = False
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"BM")
def _dib_accept(prefix: bytes) -> bool:
return i32(prefix) in [12, 40, 52, 56, 64, 108, 124]
# =============================================================================
# Image plugin for the Windows BMP format.
# =============================================================================
class BmpImageFile(ImageFile.ImageFile):
"""Image plugin for the Windows Bitmap format (BMP)"""
# ------------------------------------------------------------- Description
format_description = "Windows Bitmap"
format = "BMP"
# -------------------------------------------------- BMP Compression values
COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
for k, v in COMPRESSIONS.items():
vars()[k] = v
def _bitmap(self, header: int = 0, offset: int = 0) -> None:
"""Read relevant info about the BMP"""
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
# read bmp header size @offset 14 (this is part of the header size)
file_info: dict[str, bool | int | tuple[int, ...]] = {
"header_size": i32(read(4)),
"direction": -1,
}
# -------------------- If requested, read header at a specific position
# read the rest of the bmp header, without its size
assert isinstance(file_info["header_size"], int)
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
# ------------------------------- Windows Bitmap v2, IBM OS/2 Bitmap v1
# ----- This format has different offsets because of width/height types
# 12: BITMAPCOREHEADER/OS21XBITMAPHEADER
if file_info["header_size"] == 12:
file_info["width"] = i16(header_data, 0)
file_info["height"] = i16(header_data, 2)
file_info["planes"] = i16(header_data, 4)
file_info["bits"] = i16(header_data, 6)
file_info["compression"] = self.COMPRESSIONS["RAW"]
file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v3 to v5
# 40: BITMAPINFOHEADER
# 52: BITMAPV2HEADER
# 56: BITMAPV3HEADER
# 64: BITMAPCOREHEADER2/OS22XBITMAPHEADER
# 108: BITMAPV4HEADER
# 124: BITMAPV5HEADER
elif file_info["header_size"] in (40, 52, 56, 64, 108, 124):
file_info["y_flip"] = header_data[7] == 0xFF
file_info["direction"] = 1 if file_info["y_flip"] else -1
file_info["width"] = i32(header_data, 0)
file_info["height"] = (
i32(header_data, 4)
if not file_info["y_flip"]
else 2**32 - i32(header_data, 4)
)
file_info["planes"] = i16(header_data, 8)
file_info["bits"] = i16(header_data, 10)
file_info["compression"] = i32(header_data, 12)
# byte size of pixel data
file_info["data_size"] = i32(header_data, 16)
file_info["pixels_per_meter"] = (
i32(header_data, 20),
i32(header_data, 24),
)
file_info["colors"] = i32(header_data, 28)
file_info["palette_padding"] = 4
assert isinstance(file_info["pixels_per_meter"], tuple)
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
masks = ["r_mask", "g_mask", "b_mask"]
if len(header_data) >= 48:
if len(header_data) >= 52:
masks.append("a_mask")
else:
file_info["a_mask"] = 0x0
for idx, mask in enumerate(masks):
file_info[mask] = i32(header_data, 36 + idx * 4)
else:
# 40 byte headers only have the three components in the
# bitfields masks, ref:
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
# See also
# https://github.com/python-pillow/Pillow/issues/1293
# There is a 4th component in the RGBQuad, in the alpha
# location, but it is listed as a reserved component,
# and it is not generally an alpha channel
file_info["a_mask"] = 0x0
for mask in masks:
file_info[mask] = i32(read(4))
assert isinstance(file_info["r_mask"], int)
assert isinstance(file_info["g_mask"], int)
assert isinstance(file_info["b_mask"], int)
assert isinstance(file_info["a_mask"], int)
file_info["rgb_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
file_info["b_mask"],
)
file_info["rgba_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
file_info["b_mask"],
file_info["a_mask"],
)
else:
msg = f"Unsupported BMP header type ({file_info['header_size']})"
raise OSError(msg)
# ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16
assert isinstance(file_info["width"], int)
assert isinstance(file_info["height"], int)
self._size = file_info["width"], file_info["height"]
# ------- If color count was not found in the header, compute from bits
assert isinstance(file_info["bits"], int)
file_info["colors"] = (
file_info["colors"]
if file_info.get("colors", 0)
else (1 << file_info["bits"])
)
assert isinstance(file_info["colors"], int)
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
self._mode, raw_mode = BIT2MODE.get(file_info["bits"], ("", ""))
if not self.mode:
msg = f"Unsupported BMP pixel depth ({file_info['bits']})"
raise OSError(msg)
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
if file_info["compression"] == self.COMPRESSIONS["BITFIELDS"]:
SUPPORTED: dict[int, list[tuple[int, ...]]] = {
32: [
(0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
(0xFF000000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0xFF),
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
(0xFF000000, 0xFF00, 0xFF, 0xFF0000),
(0x0, 0x0, 0x0, 0x0),
],
24: [(0xFF0000, 0xFF00, 0xFF)],
16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
}
MASK_MODES = {
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
(32, (0xFF000000, 0xFF00, 0xFF, 0x0)): "BGXR",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
(32, (0xFF000000, 0xFF00, 0xFF, 0xFF0000)): "BGAR",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
(16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
}
if file_info["bits"] in SUPPORTED:
if (
file_info["bits"] == 32
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
):
assert isinstance(file_info["rgba_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
self._mode = "RGBA" if "A" in raw_mode else self.mode
elif (
file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
):
assert isinstance(file_info["rgb_mask"], tuple)
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
else:
msg = "Unsupported BMP bitfields layout"
raise OSError(msg)
elif file_info["compression"] == self.COMPRESSIONS["RAW"]:
if file_info["bits"] == 32 and (
header == 22 or USE_RAW_ALPHA # 32-bit .cur offset
):
raw_mode, self._mode = "BGRA", "RGBA"
elif file_info["compression"] in (
self.COMPRESSIONS["RLE8"],
self.COMPRESSIONS["RLE4"],
):
decoder_name = "bmp_rle"
else:
msg = f"Unsupported BMP compression ({file_info['compression']})"
raise OSError(msg)
# --------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
# ---------------------------------------------------- 1-bit images
if not (0 < file_info["colors"] <= 65536):
msg = f"Unsupported BMP Palette size ({file_info['colors']})"
raise OSError(msg)
else:
assert isinstance(file_info["palette_padding"], int)
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
grayscale = True
indices = (
(0, 255)
if file_info["colors"] == 2
else list(range(file_info["colors"]))
)
# ----------------- Check if grayscale and ignore palette if so
for ind, val in enumerate(indices):
rgb = palette[ind * padding : ind * padding + 3]
if rgb != o8(val) * 3:
grayscale = False
# ------- If all colors are gray, white or black, ditch palette
if grayscale:
self._mode = "1" if file_info["colors"] == 2 else "L"
raw_mode = self.mode
else:
self._mode = "P"
self.palette = ImagePalette.raw(
"BGRX" if padding == 4 else "BGR", palette
)
# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
args: list[Any] = [raw_mode]
if decoder_name == "bmp_rle":
args.append(file_info["compression"] == self.COMPRESSIONS["RLE4"])
else:
assert isinstance(file_info["width"], int)
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [
ImageFile._Tile(
decoder_name,
(0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(),
tuple(args),
)
]
def _open(self) -> None:
"""Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
if not _accept(head_data):
msg = "Not a BMP file"
raise SyntaxError(msg)
# read the start position of the BMP image data (u32)
offset = i32(head_data, 10)
# load bitmap information (offset=raster info)
self._bitmap(offset=offset)
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
rle4 = self.args[1]
data = bytearray()
x = 0
dest_length = self.state.xsize * self.state.ysize
while len(data) < dest_length:
pixels = self.fd.read(1)
byte = self.fd.read(1)
if not pixels or not byte:
break
num_pixels = pixels[0]
if num_pixels:
# encoded mode
if x + num_pixels > self.state.xsize:
# Too much data for row
num_pixels = max(0, self.state.xsize - x)
if rle4:
first_pixel = o8(byte[0] >> 4)
second_pixel = o8(byte[0] & 0x0F)
for index in range(num_pixels):
if index % 2 == 0:
data += first_pixel
else:
data += second_pixel
else:
data += byte * num_pixels
x += num_pixels
else:
if byte[0] == 0:
# end of line
while len(data) % self.state.xsize != 0:
data += b"\x00"
x = 0
elif byte[0] == 1:
# end of bitmap
break
elif byte[0] == 2:
# delta
bytes_read = self.fd.read(2)
if len(bytes_read) < 2:
break
right, up = self.fd.read(2)
data += b"\x00" * (right + up * self.state.xsize)
x = len(data) % self.state.xsize
else:
# absolute mode
if rle4:
# 2 pixels per byte
byte_count = byte[0] // 2
bytes_read = self.fd.read(byte_count)
for byte_read in bytes_read:
data += o8(byte_read >> 4)
data += o8(byte_read & 0x0F)
else:
byte_count = byte[0]
bytes_read = self.fd.read(byte_count)
data += bytes_read
if len(bytes_read) < byte_count:
break
x += byte[0]
# align to 16-bit word boundary
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
rawmode = "L" if self.mode == "L" else "P"
self.set_as_raw(bytes(data), rawmode, (0, self.args[-1]))
return -1, 0
# =============================================================================
# Image plugin for the DIB format (BMP alias)
# =============================================================================
class DibImageFile(BmpImageFile):
format = "DIB"
format_description = "Windows Bitmap"
def _open(self) -> None:
self._bitmap()
#
# --------------------------------------------------------------------
# Write BMP file
SAVE = {
"1": ("1", 1, 2),
"L": ("L", 8, 256),
"P": ("P", 8, 256),
"RGB": ("BGR", 24, 0),
"RGBA": ("BGRA", 32, 0),
}
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, False)
def _save(
im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
) -> None:
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:
msg = f"cannot write mode {im.mode} as BMP"
raise OSError(msg) from e
info = im.encoderinfo
dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches
ppm = tuple(int(x * 39.3701 + 0.5) for x in dpi)
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
header = 40 # or 64 for OS/2 version 2
image = stride * im.size[1]
if im.mode == "1":
palette = b"".join(o8(i) * 4 for i in (0, 255))
elif im.mode == "L":
palette = b"".join(o8(i) * 4 for i in range(256))
elif im.mode == "P":
palette = im.im.getpalette("RGB", "BGRX")
colors = len(palette) // 4
else:
palette = None
# bitmap header
if bitmap_header:
offset = 14 + header + colors * 4
file_size = offset + image
if file_size > 2**32 - 1:
msg = "File size is too large for the BMP format"
raise ValueError(msg)
fp.write(
b"BM" # file type (magic)
+ o32(file_size) # file size
+ o32(0) # reserved
+ o32(offset) # image data offset
)
# bitmap info header
fp.write(
o32(header) # info header size
+ o32(im.size[0]) # width
+ o32(im.size[1]) # height
+ o16(1) # planes
+ o16(bits) # depth
+ o32(0) # compression (0=uncompressed)
+ o32(image) # size of bitmap
+ o32(ppm[0]) # resolution
+ o32(ppm[1]) # resolution
+ o32(colors) # colors used
+ o32(colors) # colors important
)
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
if palette:
fp.write(palette)
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
)
#
# --------------------------------------------------------------------
# Registry
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
Image.register_save(BmpImageFile.format, _save)
Image.register_extension(BmpImageFile.format, ".bmp")
Image.register_mime(BmpImageFile.format, "image/bmp")
Image.register_decoder("bmp_rle", BmpRleDecoder)
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
Image.register_save(DibImageFile.format, _dib_save)
Image.register_extension(DibImageFile.format, ".dib")
Image.register_mime(DibImageFile.format, "image/bmp")
@@ -0,0 +1,75 @@
#
# The Python Imaging Library
# $Id$
#
# BUFR stub adapter
#
# Copyright (c) 1996-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations
import os
from typing import IO
from . import Image, ImageFile
_handler = None
def register_handler(handler: ImageFile.StubHandler | None) -> None:
"""
Install application-specific BUFR image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix: bytes) -> bool:
return prefix.startswith((b"BUFR", b"ZCZC"))
class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR"
format_description = "BUFR"
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "Not a BUFR file"
raise SyntaxError(msg)
self.fp.seek(-4, os.SEEK_CUR)
# make something up
self._mode = "F"
self._size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self) -> ImageFile.StubHandler | None:
return _handler
def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"):
msg = "BUFR save handler not installed"
raise OSError(msg)
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
Image.register_save(BufrStubImageFile.format, _save)
Image.register_extension(BufrStubImageFile.format, ".bufr")
@@ -0,0 +1,173 @@
#
# The Python Imaging Library.
# $Id$
#
# a class to read from a container file
#
# History:
# 1995-06-18 fl Created
# 1995-09-07 fl Added readline(), readlines()
#
# Copyright (c) 1997-2001 by Secret Labs AB
# Copyright (c) 1995 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations
import io
from collections.abc import Iterable
from typing import IO, AnyStr, NoReturn
class ContainerIO(IO[AnyStr]):
"""
A file object that provides read access to a part of an existing
file (for example a TAR file).
"""
def __init__(self, file: IO[AnyStr], offset: int, length: int) -> None:
"""
Create file object.
:param file: Existing file.
:param offset: Start of region, in bytes.
:param length: Size of region, in bytes.
"""
self.fh: IO[AnyStr] = file
self.pos = 0
self.offset = offset
self.length = length
self.fh.seek(offset)
##
# Always false.
def isatty(self) -> bool:
return False
def seekable(self) -> bool:
return True
def seek(self, offset: int, mode: int = io.SEEK_SET) -> int:
"""
Move file pointer.
:param offset: Offset in bytes.
:param mode: Starting position. Use 0 for beginning of region, 1
for current offset, and 2 for end of region. You cannot move
the pointer outside the defined region.
:returns: Offset from start of region, in bytes.
"""
if mode == 1:
self.pos = self.pos + offset
elif mode == 2:
self.pos = self.length + offset
else:
self.pos = offset
# clamp
self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos)
return self.pos
def tell(self) -> int:
"""
Get current file pointer.
:returns: Offset from start of region, in bytes.
"""
return self.pos
def readable(self) -> bool:
return True
def read(self, n: int = -1) -> AnyStr:
"""
Read data.
:param n: Number of bytes to read. If omitted, zero or negative,
read until end of region.
:returns: An 8-bit string.
"""
if n > 0:
n = min(n, self.length - self.pos)
else:
n = self.length - self.pos
if n <= 0: # EOF
return b"" if "b" in self.fh.mode else "" # type: ignore[return-value]
self.pos = self.pos + n
return self.fh.read(n)
def readline(self, n: int = -1) -> AnyStr:
"""
Read a line of text.
:param n: Number of bytes to read. If omitted, zero or negative,
read until end of line.
:returns: An 8-bit string.
"""
s: AnyStr = b"" if "b" in self.fh.mode else "" # type: ignore[assignment]
newline_character = b"\n" if "b" in self.fh.mode else "\n"
while True:
c = self.read(1)
if not c:
break
s = s + c
if c == newline_character or len(s) == n:
break
return s
def readlines(self, n: int | None = -1) -> list[AnyStr]:
"""
Read multiple lines of text.
:param n: Number of lines to read. If omitted, zero, negative or None,
read until end of region.
:returns: A list of 8-bit strings.
"""
lines = []
while True:
s = self.readline()
if not s:
break
lines.append(s)
if len(lines) == n:
break
return lines
def writable(self) -> bool:
return False
def write(self, b: AnyStr) -> NoReturn:
raise NotImplementedError()
def writelines(self, lines: Iterable[AnyStr]) -> NoReturn:
raise NotImplementedError()
def truncate(self, size: int | None = None) -> int:
raise NotImplementedError()
def __enter__(self) -> ContainerIO[AnyStr]:
return self
def __exit__(self, *args: object) -> None:
self.close()
def __iter__(self) -> ContainerIO[AnyStr]:
return self
def __next__(self) -> AnyStr:
line = self.readline()
if not line:
msg = "end of region"
raise StopIteration(msg)
return line
def fileno(self) -> int:
return self.fh.fileno()
def flush(self) -> None:
self.fh.flush()
def close(self) -> None:
self.fh.close()
@@ -0,0 +1,75 @@
#
# The Python Imaging Library.
# $Id$
#
# Windows Cursor support for PIL
#
# notes:
# uses BmpImagePlugin.py to read the bitmap data.
#
# history:
# 96-05-27 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations
from . import BmpImagePlugin, Image, ImageFile
from ._binary import i16le as i16
from ._binary import i32le as i32
#
# --------------------------------------------------------------------
def _accept(prefix: bytes) -> bool:
return prefix.startswith(b"\0\0\2\0")
##
# Image plugin for Windows Cursor files.
class CurImageFile(BmpImagePlugin.BmpImageFile):
format = "CUR"
format_description = "Windows Cursor"
def _open(self) -> None:
offset = self.fp.tell()
# check magic
s = self.fp.read(6)
if not _accept(s):
msg = "not a CUR file"
raise SyntaxError(msg)
# pick the largest cursor in the file
m = b""
for i in range(i16(s, 4)):
s = self.fp.read(16)
if not m:
m = s
elif s[0] > m[0] and s[1] > m[1]:
m = s
if not m:
msg = "No cursors were found"
raise TypeError(msg)
# load as bitmap
self._bitmap(i32(m, 12) + offset)
# patch up the bitmap height
self._size = self.size[0], self.size[1] // 2
d, e, o, a = self.tile[0]
self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a)
#
# --------------------------------------------------------------------
Image.register_open(CurImageFile.format, CurImageFile, _accept)
Image.register_extension(CurImageFile.format, ".cur")
@@ -0,0 +1,83 @@
#
# The Python Imaging Library.
# $Id$
#
# DCX file handling
#
# DCX is a container file format defined by Intel, commonly used
# for fax applications. Each DCX file consists of a directory
# (a list of file offsets) followed by a set of (usually 1-bit)
# PCX files.
#
# History:
# 1995-09-09 fl Created
# 1996-03-20 fl Properly derived from PcxImageFile.
# 1998-07-15 fl Renamed offset attribute to avoid name clash
# 2002-07-30 fl Fixed file handling
#
# Copyright (c) 1997-98 by Secret Labs AB.
# Copyright (c) 1995-96 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations
from . import Image
from ._binary import i32le as i32
from ._util import DeferredError
from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == MAGIC
##
# Image plugin for the Intel DCX format.
class DcxImageFile(PcxImageFile):
format = "DCX"
format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False
def _open(self) -> None:
# Header
s = self.fp.read(4)
if not _accept(s):
msg = "not a DCX file"
raise SyntaxError(msg)
# Component directory
self._offset = []
for i in range(1024):
offset = i32(self.fp.read(4))
if not offset:
break
self._offset.append(offset)
self._fp = self.fp
self.frame = -1
self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1
self.seek(0)
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
if isinstance(self._fp, DeferredError):
raise self._fp.ex
self.frame = frame
self.fp = self._fp
self.fp.seek(self._offset[frame])
PcxImageFile._open(self)
def tell(self) -> int:
return self.frame
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
Image.register_extension(DcxImageFile.format, ".dcx")

Some files were not shown because too many files have changed in this diff Show More