diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7910e4c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,14 @@ +# These are supported funding model platforms + +github: [ptmrio] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +custom: ['https://www.paypal.com/paypalme/Petermeir'] diff --git a/.gitignore b/.gitignore index d1eded8..1e69c70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ +/__pycache__/ /build/ +/dist/ /venv/ /dist/harmonized-company-names.json /dist/.env +build.py .env harmonized-company-names.json +autorename-pdf-release.zip \ No newline at end of file diff --git a/README.md b/README.md index 476d399..f681e6e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # autorename-pdf -**autorename-pdf** is a highly efficient tool designed to automatically rename and archive PDF documents based on their content. By leveraging OCR technology, it extracts critical information such as the company name, document date, and document type to create well-organized filenames. This tool simplifies document management and ensures consistency, especially for businesses handling large volumes of PDFs. +**autorename-pdf** is a highly efficient tool designed to automatically rename and archive PDF documents based on their content. By leveraging OCR and AI technology, it extracts critical information such as the company name, document date, and document type to create well-organized filenames. This tool simplifies document management and ensures consistency, especially for businesses handling large volumes of PDFs. --- @@ -11,67 +11,105 @@ - **Batch Processing**: Rename multiple PDFs within a folder in one go. - **Context Menu Integration**: Easily right-click on files or folders to trigger renaming actions. - **Powerful OCR Support**: Uses Tesseract and advanced AI via OpenAI for highly accurate text recognition from scanned PDFs. +- **Harmonized Company Names**: Converts extracted company names into a standardized format using a pre-defined mapping. --- -## Installation Guide +## Harmonized Company Names -### Prerequisites +The **harmonized company names** feature allows you to convert AI-extracted company names into a standardized format. This is particularly useful when working with various company name variants, ensuring consistent naming conventions in the output. + +For example: +- **Input**: `ACME Corp`, `ACME Inc.`, `ACME Corporation` +- **Output**: `ACME` + +This helps maintain uniformity in your archived files, improving searchability and organization. The harmonized company names are configured using a JSON file (`harmonized-company-names.json`), where you can map different variations of a company name to a standard name. + +### Example `harmonized-company-names.json`: -Ensure you have the following installed on your system: +```json +{ + "ACME": ["ACME Corp", "ACME Inc.", "ACME Corporation"], + "XYZ": ["XYZ Ltd", "XYZ LLC", "XYZ Enterprises"] +} +``` -1. **Python (OPTIONAL)**: Download and install the latest version of Python 3.x (preferably the latest version of Python 3, like 3.11): - ```powershell - winget install Python.Python - ``` +--- +## Installation Guide -2. **Chocolatey**: Required for installing dependencies on Windows. Install it using PowerShell (run as administrator): - ```powershell - Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) - ``` +### Prerequisites -2. **Tesseract OCR**: Required for extracting text from images in PDFs. Install it using winget (preferred): - ```powershell - choco install tesseract - ``` +Before starting, ensure the following: -3. **Poppler**: Required for converting PDF pages into images. Install via Chocolatey or manually: - ```powershell - choco install poppler - ``` +- **Administrator Rights**: You must run the setup as an administrator for successful installation. +- **Chocolatey, Tesseract, and Ghostscript**: These will be automatically installed if not already present. ### Setup Instructions -1. **Download or clone the Repository**: - ```cmd - git clone https://github.com/ptmrio/autorename-pdf.git - cd autorename-pdf - ``` +1. **Download the Latest Release**: + - Go to the [AutoRename-PDF GitHub Releases](https://github.com/ptmrio/autorename-pdf/releases) page. + - Download the latest `.zip` file. -2. **Edit the `.env` File**: - Configure your API key and company name by editing the `.env.example` file and move it into the dist folder as `.env.example`. Open it in any text editor and set the following: - - Add your OpenAI API key: - ``` - OPENAI_API_KEY=your-api-key +2. **Extract the ZIP Folder**: + - Extract the downloaded `.zip` file to your desired location. + +3. **Run the Setup Script**: + - Open **PowerShell with Administrator Rights**. + - Navigate to the extracted folder using the following command: + ```powershell + cd "C:\path\to\extracted\folder" ``` - - Specify your preferred OpenAI model: + - Run the setup script: + ```powershell + PowerShell -ExecutionPolicy Bypass -File .\setup.ps1 ``` - OPENAI_MODEL=gpt-4o + +4. **Follow the Installation Steps**: + - The setup script will: + - Install **Chocolatey** if not already installed. + - Install **Tesseract** and **Ghostscript** via Chocolatey. + - Add AutoRenamePDF to the context menu for files and folders. + +5. **Restart Your Computer**: + - After the installation, restart your computer to apply all context menu changes. + +--- + +## Configuration: Filling the `.env` File + +The `.env` file must be properly filled out to configure the tool. Here's a breakdown of the required parameters: + +1. **`OPENAI_API_KEY`**: + - This is your API key for accessing OpenAI's services (like GPT-4). + - You can obtain your OpenAI API key by signing up at [OpenAI](https://platform.openai.com/signup). + - After signing up, navigate to the API section and generate a new API key. Copy this key and paste it into your `.env` file like this: + ```plaintext + OPENAI_API_KEY=your-openai-api-key ``` - - Enter your company name (this prevents it from being extracted): + +2. **`OPENAI_MODEL`**: + - Specifies which OpenAI model to use for OCR and content extraction. You can use models like `gpt-3.5-turbo` or `gpt-4` for higher accuracy. + - Example: + ```plaintext + OPENAI_MODEL=gpt-4 ``` - MY_COMPANY_NAME=your-company-name + +3. **`MY_COMPANY_NAME`**: + - This is your company name, which prevents the AI from extracting it repeatedly if it's a constant in your documents. + - Example: + ```plaintext + MY_COMPANY_NAME=YourCompany ``` - Save the file as `.env` after making these changes. -3. **Run the Context Menu Setup (Administrator Required)**: - The app includes pre-built executables, so no need to install dependencies. Simply add the app to your context menu by running the following command (make sure to **run as admin**): - ```cmd - add-to-context-menu.exe - ``` +Make sure to save the `.env` file after making these changes. - This will add options to your right-click context menu for both individual PDFs and folders. +### Example `.env` File: +```plaintext +OPENAI_API_KEY=your-openai-api-key +OPENAI_MODEL=gpt-4 +MY_COMPANY_NAME=YourCompany +``` --- @@ -79,7 +117,7 @@ Ensure you have the following installed on your system: ### Context Menu (Recommended) -After installation, autorename-pdf can be accessed by right-clicking files or folders: +Once installed, autorename-pdf can be accessed through the right-click context menu: 1. **Rename a Single PDF**: Right-click a PDF file and select `Auto Rename PDF` to automatically rename it. 2. **Batch Rename PDFs in Folder**: Right-click a folder and choose `Auto Rename PDFs in Folder` to process all PDFs within. @@ -87,7 +125,7 @@ After installation, autorename-pdf can be accessed by right-clicking files or fo ### Command-Line Usage (Optional) -If you prefer using the terminal, autorename-pdf can be executed as a command-line tool: +For command-line users, autorename-pdf can also be executed from the terminal: - **Rename a single PDF**: ```bash diff --git a/add-to-context-menu.py b/add-to-context-menu.py deleted file mode 100644 index 779e4f1..0000000 --- a/add-to-context-menu.py +++ /dev/null @@ -1,123 +0,0 @@ -import os -import sys -import winreg as reg -import ctypes - -def is_admin(): - try: - return ctypes.windll.shell32.IsUserAnAdmin() - except: - return False - -def add_registry_entries(): - if not is_admin(): - print("This script requires administrator privileges. Please run as administrator.") - return - - # Get the current directory - current_directory = os.path.dirname(os.path.abspath(__file__)) - - # Check if we're running from source or as a built executable - if getattr(sys, 'frozen', False): - # We're running in a bundle (built executable) - current_directory = os.path.dirname(sys.executable) - main_script = os.path.join(current_directory, "autorename-pdf.exe") # autorename-pdf.exe should be alongside this executable - else: - # We're running in a normal Python environment - executable = os.path.join(current_directory, "venv", "Scripts", "python.exe") - main_script = os.path.join(current_directory, "autorename.py") - - # Command for folders (using the main script directly) - if getattr(sys, 'frozen', False): - autorename_command = f'"{main_script}" "%1"' - else: - autorename_command = f'"{executable}" "{main_script}" "%1"' - - # Confirm with the user - confirm = input("This will add 'Auto Rename PDF' to your context menus. Continue? (y/n): ") - if confirm.lower() != 'y': - print("Operation cancelled.") - return - - try: - # Add registry entries for PDFs (using the wrapper) - add_menu_for_file_type("SystemFileAssociations\\.pdf", "Auto Rename PDF", autorename_command) - - # Add registry entries for Folders (using the main script) - add_menu_for_folder("Auto Rename PDFs in Folder", autorename_command) - - # Add registry entries for Directory Background (using the main script) - add_menu_for_directory_background("Auto Rename PDFs in This Folder", autorename_command) - - print("Registry entries added successfully.") - except Exception as e: - print(f"An error occurred: {e}") - -def add_menu_for_file_type(file_type_key, menu_name, command): - key_path = f"{file_type_key}\\shell\\AutoRenamePDF" - key_command_path = f"{key_path}\\command" - - with reg.CreateKey(reg.HKEY_CLASSES_ROOT, key_path) as key: - reg.SetValueEx(key, None, 0, reg.REG_SZ, menu_name) - reg.SetValueEx(key, "Icon", 0, reg.REG_SZ, "shell32.dll,71") - - with reg.CreateKey(reg.HKEY_CLASSES_ROOT, key_command_path) as key: - reg.SetValueEx(key, None, 0, reg.REG_SZ, command) - -def add_menu_for_folder(menu_name, command): - key_path = r"Directory\shell\AutoRenamePDFs" - key_command_path = f"{key_path}\\command" - - with reg.CreateKey(reg.HKEY_CLASSES_ROOT, key_path) as key: - reg.SetValueEx(key, None, 0, reg.REG_SZ, menu_name) - reg.SetValueEx(key, "Icon", 0, reg.REG_SZ, "shell32.dll,71") - - with reg.CreateKey(reg.HKEY_CLASSES_ROOT, key_command_path) as key: - reg.SetValueEx(key, None, 0, reg.REG_SZ, command) - -def add_menu_for_directory_background(menu_name, command): - key_path = r"Directory\Background\shell\AutoRenamePDFs" - key_command_path = f"{key_path}\\command" - - with reg.CreateKey(reg.HKEY_CLASSES_ROOT, key_path) as key: - reg.SetValueEx(key, None, 0, reg.REG_SZ, menu_name) - reg.SetValueEx(key, "Icon", 0, reg.REG_SZ, "shell32.dll,71") - - with reg.CreateKey(reg.HKEY_CLASSES_ROOT, key_command_path) as key: - reg.SetValueEx(key, None, 0, reg.REG_SZ, command.replace('"%1"', '"%V"')) - -def remove_registry_entries(): - if not is_admin(): - print("This script requires administrator privileges. Please run as administrator.") - return - - confirm = input("This will remove 'Auto Rename PDF' from your context menus. Continue? (y/n): ") - if confirm.lower() != 'y': - print("Operation cancelled.") - return - - try: - # Remove entries for PDFs - reg.DeleteKey(reg.HKEY_CLASSES_ROOT, r"SystemFileAssociations\.pdf\shell\AutoRenamePDF\command") - reg.DeleteKey(reg.HKEY_CLASSES_ROOT, r"SystemFileAssociations\.pdf\shell\AutoRenamePDF") - - # Remove entries for Folders - reg.DeleteKey(reg.HKEY_CLASSES_ROOT, r"Directory\shell\AutoRenamePDFs\command") - reg.DeleteKey(reg.HKEY_CLASSES_ROOT, r"Directory\shell\AutoRenamePDFs") - - # Remove entries for Directory Background - reg.DeleteKey(reg.HKEY_CLASSES_ROOT, r"Directory\Background\shell\AutoRenamePDFs\command") - reg.DeleteKey(reg.HKEY_CLASSES_ROOT, r"Directory\Background\shell\AutoRenamePDFs") - - print("Registry entries removed successfully.") - except Exception as e: - print(f"An error occurred: {e}") - -if __name__ == "__main__": - action = input("Do you want to (a)dd or (r)emove registry entries? ").lower() - if action == 'a': - add_registry_entries() - elif action == 'r': - remove_registry_entries() - else: - print("Invalid option. Please choose 'a' to add or 'r' to remove.") \ No newline at end of file diff --git a/add-to-context-menu.spec b/add-to-context-menu.spec deleted file mode 100644 index 7581b42..0000000 --- a/add-to-context-menu.spec +++ /dev/null @@ -1,38 +0,0 @@ -# -*- mode: python ; coding: utf-8 -*- - - -a = Analysis( - ['add-to-context-menu.py'], - pathex=[], - binaries=[], - datas=[], - hiddenimports=[], - hookspath=[], - hooksconfig={}, - runtime_hooks=[], - excludes=[], - noarchive=False, - optimize=0, -) -pyz = PYZ(a.pure) - -exe = EXE( - pyz, - a.scripts, - a.binaries, - a.datas, - [], - name='add-to-context-menu', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=True, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch=None, - codesign_identity=None, - entitlements_file=None, -) diff --git a/autorename-pdf.py b/autorename-pdf.py new file mode 100644 index 0000000..a4b3986 --- /dev/null +++ b/autorename-pdf.py @@ -0,0 +1,56 @@ +import os +import sys +import logging +from dotenv import load_dotenv +from pdf_processor import process_pdf, PDF_EXTENSION, initialize_openai_client + + +# Configure logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +if getattr(sys, 'frozen', False): + current_directory = os.path.dirname(sys.executable) # Path to the folder containing the .exe +else: + current_directory = os.path.dirname(os.path.abspath(__file__)) # Path to the script file + +# Define the path to the .env file +env_path = os.path.join(current_directory, '.env') + +# Define the path to the harmonized-company-names.json file +json_path = os.path.join(current_directory, 'harmonized-company-names.json') + + +# Load environment variables from the .env file +load_dotenv(env_path) + +# Initialize OpenAI client +openai_api_key = os.getenv("OPENAI_API_KEY") +if not openai_api_key: + logging.error("OPENAI_API_KEY not found in environment variables.") + sys.exit(1) + +initialize_openai_client(openai_api_key) + +def process_input(input_paths): + """Process multiple input paths, which can be files or folders.""" + for input_path in input_paths: + if os.path.isfile(input_path): + if input_path.lower().endswith(PDF_EXTENSION): + process_pdf(input_path, json_path) + else: + logging.warning(f"{input_path} is not a valid PDF.") + elif os.path.isdir(input_path): + for root, _, files in os.walk(input_path): + for file in files: + if file.lower().endswith(PDF_EXTENSION): + process_pdf(os.path.join(root, file), json_path) + else: + logging.error(f"{input_path} is not a valid file or folder.") + +if __name__ == "__main__": + if len(sys.argv) < 2: + logging.error('Usage: python autorename.py [ ...]') + sys.exit(1) + + input_paths = sys.argv[1:] + process_input(input_paths) \ No newline at end of file diff --git a/dist/add-to-context-menu.exe b/dist/add-to-context-menu.exe deleted file mode 100644 index 025d26a..0000000 Binary files a/dist/add-to-context-menu.exe and /dev/null differ diff --git a/dist/autorename-pdf.exe b/dist/autorename-pdf.exe deleted file mode 100644 index 2f8fe31..0000000 Binary files a/dist/autorename-pdf.exe and /dev/null differ diff --git a/autorename.py b/pdf_processor.py similarity index 60% rename from autorename.py rename to pdf_processor.py index 1c4c0df..9dd0ea3 100644 --- a/autorename.py +++ b/pdf_processor.py @@ -3,17 +3,16 @@ import logging from typing import Dict, Tuple, Optional from jellyfish import jaro_winkler_similarity -from dotenv import load_dotenv -from pdf2image import convert_from_path import datetime -import pytesseract +import ocrmypdf +from ocrmypdf import exceptions as ocrmypdf_exceptions from openai import OpenAI import json import dateparser import re -from PIL import Image -import cv2 -import numpy as np +import fitz # PyMuPDF +import tempfile +import traceback from pydantic import BaseModel, Field # Constants @@ -22,30 +21,23 @@ DEFAULT_DATE = "00000000" CONFIDENCE_THRESHOLD = 0.85 -# Configure logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +client = None if getattr(sys, 'frozen', False): current_directory = os.path.dirname(sys.executable) # Path to the folder containing the .exe else: current_directory = os.path.dirname(os.path.abspath(__file__)) # Path to the script file -# Define the path to the .env file -env_path = os.path.join(current_directory, '.env') - -# Load environment variables from the .env file -load_dotenv(env_path) - -openai_model = os.getenv("OPENAI_MODEL") -my_company_name = os.getenv("MY_COMPANY_NAME") - -client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) +def initialize_openai_client(api_key): + global client + client = OpenAI(api_key=api_key) class DocumentResponse(BaseModel): company_name: str = Field(..., description="Name of the company in the document") document_date: str = Field(..., description="Date of the document in format dd.mm.yyyy") document_type: str = Field(..., description="Type of the document (ER, AR, etc.)") + def is_valid_filename(filename: str) -> bool: forbidden_chars = r'[<>:"/\\|?*]' @@ -60,57 +52,60 @@ def is_valid_filename(filename: str) -> bool: return True -def preprocess_image(image): - # Convert to grayscale - gray = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2GRAY) - # Apply thresholding to preprocess the image - gray = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] - # Apply dilation to remove noise - kernel = np.ones((1, 1), np.uint8) - gray = cv2.dilate(gray, kernel, iterations=1) - return Image.fromarray(gray) - -def pdf_to_text(pdf_path: str, start_page: int = 1, end_page: int = 1) -> str: - """Convert a range of pages from a PDF to text using OCR with preprocessing.""" + +def pdf_to_text(pdf_path: str, start_page: int = 1, end_page: int = 3) -> str: + """Process PDF: always run OCR first, then extract text using PyMuPDF.""" + all_text = "" + try: - images = convert_from_path(pdf_path, first_page=start_page, last_page=end_page) - text = "" - for image in images: - # Preprocess the image - processed_image = preprocess_image(image) - # Perform OCR with specific configuration - page_text = pytesseract.image_to_string( - processed_image, - config='--psm 6 --oem 3 -c preserve_interword_spaces=1' + # Step 1: Always run OCR + with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as temp_file: + temp_path = temp_file.name + + try: + ocrmypdf.ocr( + pdf_path, + temp_path, + force_ocr=True, + pages=f"{start_page}-{end_page}", + language=['eng', 'deu'], + invalidate_digital_signatures=True ) - text += page_text + "\n\n" - return text.strip() + except ocrmypdf_exceptions.PriorOcrFoundError: + logging.warning(f"Prior OCR found in {pdf_path}. Skipping OCR step.") + temp_path = pdf_path + + # Step 2: Extract text using PyMuPDF (fitz) + doc = fitz.open(temp_path) + for page_num in range(start_page - 1, min(end_page, len(doc))): + page = doc[page_num] + page_text = page.get_text() + all_text += f"Page {page_num + 1}:\n{page_text}\n\n" + doc.close() + + # Clean up temporary file + os.unlink(temp_path) + except Exception as e: - logging.error(f"Error converting PDF to text: {e}") - return "" + logging.error(f"Error processing PDF {pdf_path}: {str(e)}") -def get_openai_response(pdf_path: str) -> Dict[str, str]: - """Get structured information from OpenAI API, potentially using multiple pages.""" - text = pdf_to_text(pdf_path, start_page=1, end_page=1) - response = process_text_with_openai(text) - - if response['company_name'] == UNKNOWN_VALUE or response['document_date'] == DEFAULT_DATE or response['document_type'] == UNKNOWN_VALUE: - logging.info("Insufficient information from first page. Checking second page.") - text += "\n\n" + pdf_to_text(pdf_path, start_page=2, end_page=2) - response = process_text_with_openai(text) - - if response['company_name'] == UNKNOWN_VALUE or response['document_date'] == DEFAULT_DATE or response['document_type'] == UNKNOWN_VALUE: - logging.info("Still insufficient information. Checking third page.") - text += "\n\n" + pdf_to_text(pdf_path, start_page=3, end_page=3) - response = process_text_with_openai(text) + logging.info(f"Extracted text (first 500 characters): {all_text[:500]}...") - return response + if not all_text.strip(): + logging.warning(f"No text extracted from {pdf_path}") + return all_text.strip() + + def process_text_with_openai(text: str) -> Dict[str, str]: """Process the extracted text with OpenAI API.""" + global client + if client is None: + raise ValueError("OpenAI client not initialized. Call initialize_openai_client first.") + try: response = client.chat.completions.create( - model=os.getenv("OPENAI_MODEL", "gpt-4o"), + model=os.getenv("OPENAI_MODEL", "gpt-4"), messages=[ { "role": "system", @@ -153,14 +148,14 @@ def process_text_with_openai(text: str) -> Dict[str, str]: return {"company_name": UNKNOWN_VALUE, "document_date": DEFAULT_DATE, "document_type": UNKNOWN_VALUE} -def harmonize_company_name(company_name: str) -> str: +def harmonize_company_name(company_name: str, json_path: str) -> str: """Harmonize company name based on predefined mappings.""" company_name = company_name.strip() - if not os.path.exists("harmonized-company-names.json"): - logging.warning(f'harmonized-company-names.json not found, using original name: {company_name}') + if not os.path.exists(json_path): + logging.warning(f'{json_path} not found, using original name: {company_name}') return company_name - with open("harmonized-company-names.json", "r", encoding='utf-8') as file: + with open(json_path, "r", encoding='utf-8') as file: harmonized_names = json.load(file) best_match = max( @@ -214,35 +209,20 @@ def rename_invoice(pdf_path: str, company_name: str, document_date: Optional[dat except Exception as e: logging.error(f'Error renaming {pdf_path}: {str(e)}') -def process_pdf(pdf_path: str) -> None: +def process_pdf(pdf_path: str, json_path: str) -> None: """Process a single PDF file.""" logging.info("---") logging.info(f"Processing {pdf_path}") - openai_response = get_openai_response(pdf_path) - company_name, document_date, document_type = parse_openai_response(openai_response) - company_name = harmonize_company_name(company_name) - rename_invoice(pdf_path, company_name, document_date, document_type) - -def process_input(input_paths): - """Process multiple input paths, which can be files or folders.""" - for input_path in input_paths: - if os.path.isfile(input_path): - if input_path.lower().endswith(PDF_EXTENSION): - process_pdf(input_path) - else: - logging.warning(f"{input_path} is not a valid PDF.") - elif os.path.isdir(input_path): - for root, _, files in os.walk(input_path): - for file in files: - if file.lower().endswith(PDF_EXTENSION): - process_pdf(os.path.join(root, file)) - else: - logging.error(f"{input_path} is not a valid file or folder.") - -if __name__ == "__main__": - if len(sys.argv) < 2: - logging.error('Usage: python autorename.py [ ...]') - sys.exit(1) - - input_paths = sys.argv[1:] - process_input(input_paths) \ No newline at end of file + try: + extracted_text = pdf_to_text(pdf_path) + if not extracted_text: + logging.warning(f"No text extracted from {pdf_path}. Skipping further processing.") + return + + openai_response = process_text_with_openai(extracted_text) + company_name, document_date, document_type = parse_openai_response(openai_response) + company_name = harmonize_company_name(company_name, json_path) + rename_invoice(pdf_path, company_name, document_date, document_type) + except Exception as e: + logging.error(f"Error processing {pdf_path}: {str(e)}") + logging.debug(traceback.format_exc()) diff --git a/requirements.txt b/requirements.txt index 766fa16..8fcd587 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +altgraph==0.17.4 annotated-types==0.7.0 anyio==4.4.0 certifi==2024.8.30 @@ -6,32 +7,50 @@ charset-normalizer==3.3.2 colorama==0.4.6 cryptography==43.0.1 dateparser==1.2.0 +Deprecated==1.2.14 +deprecation==2.1.0 distro==1.9.0 h11==0.14.0 httpcore==1.0.5 httpx==0.27.2 idna==3.8 +img2pdf==0.5.1 jellyfish==1.1.0 jiter==0.5.0 -numpy==2.1.1 +lxml==5.3.0 +markdown-it-py==3.0.0 +mdurl==0.1.2 +ocrmypdf==16.5.0 openai==1.43.0 opencv-python-headless==4.10.0.84 packaging==24.1 pdf2image==1.17.0 pdfminer.six==20231228 +pefile==2024.8.26 +pi_heif==0.18.0 +pikepdf==9.2.1 pillow==10.4.0 +pluggy==1.5.0 pycparser==2.22 pydantic==2.8.2 pydantic_core==2.20.1 +Pygments==2.18.0 +pyinstaller==6.10.0 +pyinstaller-hooks-contrib==2024.8 +PyMuPDF==1.24.10 +PyMuPDFb==1.24.10 pypdfium2==4.30.0 -pytesseract==0.3.13 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 pytz==2024.1 +pywin32-ctypes==0.2.3 regex==2024.7.24 +rich==13.8.0 +setuptools==74.1.2 six==1.16.0 sniffio==1.3.1 tqdm==4.66.5 typing_extensions==4.12.2 tzdata==2024.1 tzlocal==5.2 +wrapt==1.16.0 diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..ebe9009 --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,358 @@ +# AutoRenamePDF Installation/Uninstallation Script + +# Check if running as administrator +if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Warning "You do not have Administrator rights to run this script!`nPlease re-run this script as an Administrator!" + Break +} + +# Function to find the autorename-pdf.exe +function Find-AutoRenamePDF { + $scriptPath = $PSScriptRoot + $exePath = Join-Path $scriptPath "autorename-pdf.exe" + if (Test-Path $exePath) { + return $exePath + } + else { + Write-Error "autorename-pdf.exe not found in the script directory. Please ensure it's in the same folder as this script." + exit + } +} + +# Function to install Chocolatey +function Install-Chocolatey { + Set-ExecutionPolicy Bypass -Scope Process -Force + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 + Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) +} + +# Function to install a package using Chocolatey +function Install-ChocoPackage { + param($packageName) + choco install $packageName -y +} + +# Function to download and install German language data for Tesseract +function Install-TesseractGermanData { + $tessdataPath = "C:\Program Files\Tesseract-OCR\tessdata" + $germanDataUrl = "https://github.com/tesseract-ocr/tessdata/raw/main/deu.traineddata" + $germanDataFile = Join-Path $tessdataPath "deu.traineddata" + + if (-not (Test-Path $tessdataPath)) { + Write-Error "Tesseract tessdata directory not found. Make sure Tesseract is installed correctly." + return + } + + Write-Host "Downloading German language data for Tesseract..." -ForegroundColor Yellow + Invoke-WebRequest -Uri $germanDataUrl -OutFile $germanDataFile + + if (Test-Path $germanDataFile) { + Write-Host "German language data installed successfully." -ForegroundColor Green + } + else { + Write-Error "Failed to download German language data." + } +} + +# Function to add registry entries for AutoRenamePDF +function Add-RegistryEntries { + param($exePath) + + # For PDF files + New-Item -Path "HKLM:\SOFTWARE\Classes\SystemFileAssociations\.pdf\shell\AutoRenamePDF" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\SystemFileAssociations\.pdf\shell\AutoRenamePDF" -Name "(Default)" -Value "Auto Rename PDF" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\SystemFileAssociations\.pdf\shell\AutoRenamePDF" -Name "Icon" -Value "shell32.dll,71" + New-Item -Path "HKLM:\SOFTWARE\Classes\SystemFileAssociations\.pdf\shell\AutoRenamePDF\command" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\SystemFileAssociations\.pdf\shell\AutoRenamePDF\command" -Name "(Default)" -Value "`"$exePath`" `"%1`"" + + # For Folders + New-Item -Path "HKLM:\SOFTWARE\Classes\Directory\shell\AutoRenamePDFs" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Directory\shell\AutoRenamePDFs" -Name "(Default)" -Value "Auto Rename PDFs in Folder" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Directory\shell\AutoRenamePDFs" -Name "Icon" -Value "shell32.dll,71" + New-Item -Path "HKLM:\SOFTWARE\Classes\Directory\shell\AutoRenamePDFs\command" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Directory\shell\AutoRenamePDFs\command" -Name "(Default)" -Value "`"$exePath`" `"%1`"" + + # For Directory Background + New-Item -Path "HKLM:\SOFTWARE\Classes\Directory\Background\shell\AutoRenamePDFs" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Directory\Background\shell\AutoRenamePDFs" -Name "(Default)" -Value "Auto Rename PDFs in This Folder" + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Directory\Background\shell\AutoRenamePDFs" -Name "Icon" -Value "shell32.dll,71" + New-Item -Path "HKLM:\SOFTWARE\Classes\Directory\Background\shell\AutoRenamePDFs\command" -Force + Set-ItemProperty -Path "HKLM:\SOFTWARE\Classes\Directory\Background\shell\AutoRenamePDFs\command" -Name "(Default)" -Value "`"$exePath`" `"%V`"" +} + +# Function to remove registry entries +function Remove-RegistryEntries { + Remove-Item -Path "HKLM:\SOFTWARE\Classes\SystemFileAssociations\.pdf\shell\AutoRenamePDF" -Recurse -ErrorAction SilentlyContinue + Remove-Item -Path "HKLM:\SOFTWARE\Classes\Directory\shell\AutoRenamePDFs" -Recurse -ErrorAction SilentlyContinue + Remove-Item -Path "HKLM:\SOFTWARE\Classes\Directory\Background\shell\AutoRenamePDFs" -Recurse -ErrorAction SilentlyContinue +} + +# Main routine +$exePath = Find-AutoRenamePDF + +$choice = Read-Host "Do you want to (I)nstall or (U)ninstall AutoRenamePDF? (I/U)" + +if ($choice -eq "I" -or $choice -eq "i") { + Write-Host "Starting AutoRenamePDF installation..." -ForegroundColor Green + + # Install Chocolatey if not already installed + if (!(Get-Command choco -ErrorAction SilentlyContinue)) { + Write-Host "Installing Chocolatey..." -ForegroundColor Yellow + Install-Chocolatey + refreshenv + } + else { + Write-Host "Chocolatey is already installed." -ForegroundColor Green + } + + # Install Tesseract + Write-Host "Installing Tesseract..." -ForegroundColor Yellow + Install-ChocoPackage "tesseract" + + # Install German language data for Tesseract + Install-TesseractGermanData + + # Install Ghostscript + Write-Host "Installing Ghostscript..." -ForegroundColor Yellow + Install-ChocoPackage "ghostscript" + + # Add registry entries + Write-Host "Adding AutoRenamePDF to context menu..." -ForegroundColor Yellow + Add-RegistryEntries -exePath $exePath + + Write-Host "Installation completed successfully!" -ForegroundColor Green + Write-Host "Please restart your computer to ensure all changes take effect." -ForegroundColor Yellow +} +elseif ($choice -eq "U" -or $choice -eq "u") { + Write-Host "Starting AutoRenamePDF uninstallation..." -ForegroundColor Green + + # Remove registry entries + Write-Host "Removing AutoRenamePDF from context menu..." -ForegroundColor Yellow + Remove-RegistryEntries + + Write-Host "Uninstallation completed successfully!" -ForegroundColor Green + Write-Host "The registry entries for AutoRenamePDF have been removed." -ForegroundColor Yellow + Write-Host "Tesseract and Ghostscript have not been uninstalled. If you wish to remove them, please use Chocolatey or the Windows Control Panel." -ForegroundColor Yellow +} +else { + Write-Host "Invalid choice. Please run the script again and choose 'I' to install or 'U' to uninstall." -ForegroundColor Red +} +# SIG # Begin signature block +# MIIooAYJKoZIhvcNAQcCoIIokTCCKI0CAQExDzANBglghkgBZQMEAgEFADB5Bgor +# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBoP0xLLppRwKcK +# SfgIjbX94+sEePGecg4BpFueAJ2RoqCCDaAwgga5MIIEoaADAgECAhEAmaOACiZV +# O2Wr3G6EprPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMCUEwxIjAgBgNV +# BAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1bSBD +# ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQg +# TmV0d29yayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUxODA1MzIxOFowVjEL +# MAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEk +# MCIGA1UEAxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENBMIICIjANBgkqhkiG +# 9w0BAQEFAAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5jXXp1ur5cTbq96y34 +# vuTmflN4mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS4IndU8s7VS5+djSo +# McbvIKck6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27a8RdrT1PH9ud0IF+ +# njvMk2xqbNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxYDB7SYOouu9Tj1yHI +# ohzuC8KNqfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEmhu1gMXgv8aGUsRda +# CtVD2bSlbfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQN9n/kZtMLK+Wo837 +# Q4QOZgYqVWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3HybZtYvj9lDV2nT8m +# FSkcSkAExzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOIG5knN4Q/JHuurfTI +# 5XDYO962WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g7qf/rCkKbWpQ5bou +# fUnq1UiYPIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpwZo4SlADaCi2JSplK +# ShBSND36E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rBJAcOSnAWd18Jx5n8 +# 58JSqPECAwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFN10 +# XUwA23ufoHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkCw6A/joq8+tT4HKbR +# Og79MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcDAzAwBgNVHR8E +# KTAnMCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIuY3JsMGwGCCsG +# AQUFBwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9jc3AtY2VydHVt +# LmNvbTAyBggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2VydHVtLnBsL2N0 +# bmNhMi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUHAgEWGGh0dHA6 +# Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEAdYhYD+WPUCia +# U58Q7EP89DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARMgwRzLRUStJl490L9 +# 4C9LGF3vjzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJTZVJeChDUyuQy6rG +# DxLUUAsO0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNGxHBIutiRsWrhWM2f +# 8pXdd3x2mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQigO0ugh7bWRLDm0Cd +# Y9rNLqyA3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4Am1/7mATwZWwSD+B +# 7eMcZNhpn8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lIe5Iv6RY3/bFAEloM +# U+vUBfSouCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7gCBibESYkOh1/w1t +# VxTpV2Na3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOEMLOGGniR+x+zPF/2 +# DaGgK2W1eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LHuH8X3n9C+xN4YaNj +# t2ywzOr+tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyYeEEUz/Qwt4HOUBCr +# W602NCmvO1nm+/80nLy5r0AZvCQxaQ4wggbfMIIEx6ADAgECAhA5MbTyzEucC8ur +# CrsHvepjMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEwHwYDVQQKExhB +# c3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1bSBDb2RlIFNp +# Z25pbmcgMjAyMSBDQTAeFw0yNDA4MDExMjA2MDVaFw0yNTA4MDExMjA2MDRaMIGE +# MQswCQYDVQQGEwJBVDEPMA0GA1UECAwGU3R5cmlhMREwDwYDVQQHDAhGZWxkYmFj +# aDEeMBwGA1UECgwVT3BlbiBTb3VyY2UgRGV2ZWxvcGVyMTEwLwYDVQQDDChPcGVu +# IFNvdXJjZSBEZXZlbG9wZXIsIEdlcmhhcmQgUGV0ZXJtZWlyMIICIjANBgkqhkiG +# 9w0BAQEFAAOCAg8AMIICCgKCAgEAoyE+k/6tkcIEzw6/wJuIHLmpvVX+KaP3Nh+u +# kb1Ugo9OSho3mHsk1lTZCKpCP4smmjQyfOeDo1qcc5oZS8DBLfLUu/gcqTPyFIvR +# +ZILzTjPQLY1ft1Vy06IjBRfOq4VgezGdpqHgg5hjGBsBDkP1crgz+dgSJ9sS2x5 +# U1e5WkAeBNBdgmvineFK5C2UbZJvyohSfnlfJVr2nYH/PpyuyxwaJgm+eoVpgBAG +# qK79ZGsiRaQD5LRi8x6edDJXSy1y0sTnwpicXEH+qZPPZvlalOO4QpH9aqk1gVcS +# wu0Ji/WCcj7XsILaKnKmTnXSI+w8EH9bC6p38eFpd69E5lQcdpRTchtCr+HdQB/J +# gUp8IA8PkDU2FCoaFbkKPOayiZL+5zV3erVuRNLu7lB8v5WX6S0mNJMSCoKDBWwl +# wcSI5bnmOHjBOhaUYBk8EmZz3esyijnIQvOXlJGIEWEoTFKUk6fseEsXIAEiAzLQ +# 7KOmmyG3t3kzNplBfFnJ56mqoBCdqco9m4XqBIYCALQtN9saeFPYNXBA6zhVIsmB +# Lbo/rjmvDR1G0U6CXdyXnUMHCUUfNV5YhyfrfLe/wmI2ECIAO4/JxO6UDOx+rUjQ +# Ostt62fewtKpXabK1yeqdLL7ImwdMGTmmg0TrYvPR4rw/jhSR+wTfmVfefxB4lBW +# NRr3CIUCAwEAAaOCAXgwggF0MAwGA1UdEwEB/wQCMAAwPQYDVR0fBDYwNDAyoDCg +# LoYsaHR0cDovL2Njc2NhMjAyMS5jcmwuY2VydHVtLnBsL2Njc2NhMjAyMS5jcmww +# cwYIKwYBBQUHAQEEZzBlMCwGCCsGAQUFBzABhiBodHRwOi8vY2NzY2EyMDIxLm9j +# c3AtY2VydHVtLmNvbTA1BggrBgEFBQcwAoYpaHR0cDovL3JlcG9zaXRvcnkuY2Vy +# dHVtLnBsL2Njc2NhMjAyMS5jZXIwHwYDVR0jBBgwFoAU3XRdTADbe5+gdMqxbvc8 +# wDLAcM0wHQYDVR0OBBYEFPLIIsE2cJ/rfmRif21ohtvUgOM3MEsGA1UdIAREMEIw +# CAYGZ4EMAQQBMDYGCyqEaAGG9ncCBQEEMCcwJQYIKwYBBQUHAgEWGWh0dHBzOi8v +# d3d3LmNlcnR1bS5wbC9DUFMwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDgYDVR0PAQH/ +# BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBA0D8td6CBusw+WPnGUL+KOh90o0Os +# 8yoFyk2RPw2uGpJrElwMaaC+ZXbwi4C/6fqErab5I0MOX90S3ItWjou2XYPUD8/6 +# ycRTdRDFKXq6J1Az/1H1g8DU+CeTK3g6WVrLLnDyO1Nbg/Hw81W38vH3b3Wl7Z6d +# HR1FvElFXeKpQHjzvXHKNRX5vJFbAwqCiNs54kaNqsm3Ced/nrbMdC1M7qX64S3W +# x7EdnrX+L0X3fS58jG/+/2W0aBbw6c+Oyk8PiblUZrePWZ3JcZEBVSIzrbCQPYAh +# DMAksOLOPefFCW4rFQYN80TlNf4OMvdE4+d6xS9rdJcrCcnD32+YEjUVMK8bGClk +# y7DCstkMsFlTW+QoouFjBGNZhpoYzCwsMtHw2xCZl4AhhjUdKm084an4Jl/+pA1n +# TElTr0izCfO+NT8d9SEwMYY0994nxW2Vy9B7+L8MaNnBDAJUkYUuSjrrYDIk++oO +# 5THnS0knisZYqx1mzRieP7X6IznIUx+tbbM0/t/npH88sfT4cVP+wSVGXi7cd9gA +# 8EobelW3SHau+tZFRakbn5Of0Okmz0E10MvptRUd2GWLQ5Su4dRmbrAGxOQpVhTK +# 6zJmA9IetArs9Q2WefWTUQwomJGp0m8ZofGrqWUM9ZnbcvLQ3x+o9KhOdqsxxG1R +# P5oqJ2htYqYXyDGCGlYwghpSAgEBMGowVjELMAkGA1UEBhMCUEwxITAfBgNVBAoT +# GEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5BLjEkMCIGA1UEAxMbQ2VydHVtIENvZGUg +# U2lnbmluZyAyMDIxIENBAhA5MbTyzEucC8urCrsHvepjMA0GCWCGSAFlAwQCAQUA +# oHwwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw +# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIOOx +# 68t5OjQaebe06BQcbFz+mSGK65uiOK63l1M22i7EMA0GCSqGSIb3DQEBAQUABIIC +# AG0B3gh4DaLsSHiqd0oUNQsY819w8fYVD2wgUypJowMrX/Nwb7ycSu97/xZi4I0Q +# FFihdk1uoXz41K0p+bomkmlxU2YP8cFBFu5cMe5VuqiEr/aCQc4+ZKonLT6eGd9d +# c7JSKl9fokHh0zmW+TfVcbslRLOxfJcKSyDU8YlwfQgXV2rZUXBx+8qk7xUWtGVR +# 1H5ccK18kQ1px1+nscIx3HvHW0wiLisqiXB9m3UztSrYAoh8Rd45c3Jgm5q2iZSW +# 6L6n35e/X5+ccXh1qsy5kQ3tatEv+3RTplFvfkQJrzDBFjZUEB7D0jZoky9ENA9W +# LlxhUHoK1MtSePZUTOQiSLL0XRfyieAhOmTKtEeh1aCdghRPQNZHf3DVpU0pnqtq +# LbMg6PSjhfk94LQpQWWLuV+G+G7hFT4NSgR8bj0WAnlXCdzj8j9rOvflvO8WXiLn +# zsLIE8Xm35JMlLweLx6pNeWzpfZEXi1bAJFebrjLBQj66PlzWoHDQviE/1Jy8qYY +# ImwJMheReOpEnV1gZaKaZ58p8hnzAgNfyPqYVR4jgOXU8IbGaL4AzIQouUGURBx1 +# XLlcjArXcNUwaDwor6pxnYtv5KnLcaK3J88SelRF9y7aGgb3lfBbIQEPptB0vab8 +# +aCydYFOfEdvyH5E/0fEF851KXQh4kkahB2BweOBfDINoYIXPzCCFzsGCisGAQQB +# gjcDAwExghcrMIIXJwYJKoZIhvcNAQcCoIIXGDCCFxQCAQMxDzANBglghkgBZQME +# AgEFADB3BgsqhkiG9w0BCRABBKBoBGYwZAIBAQYJYIZIAYb9bAcBMDEwDQYJYIZI +# AWUDBAIBBQAEIPQbv6d/e/JjY9ji3V8LgdBEauaSHfxOOfvUJsWmuyp7AhAZ4ilS +# O3SXXIU2WvmWnJVmGA8yMDI0MDkwNjEwNDQwN1qgghMJMIIGwjCCBKqgAwIBAgIQ +# BUSv85SdCDmmv9s/X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX +# MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 +# ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAw +# MDAwMFoXDTM0MTAxMzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRp +# Z2lDZXJ0LCBJbmMuMSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCC +# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X +# 5dLnXaEOCdwvSKOXejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uU +# UI8cIOrHmjsvlmbjaedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa +# 2mq62DvKXd4ZGIX7ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgt +# XkV1lnX+3RChG4PBuOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60 +# pCFkcOvV5aDaY7Mu6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17 +# cz4y7lI0+9S769SgLDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BY +# QfvYsSzhUa+0rRUGFOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9 +# c33u3Qr/eTQQfqZcClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw +# 9/sqhux7UjipmAmhcbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2c +# kpMEtGlwJw1Pt7U20clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhR +# B8qUt+JQofM604qDy0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYD +# VR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgG +# BmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxq +# II+eyG8wHQYDVR0OBBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEw +# T6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH +# NFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGD +# MIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYB +# BQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0 +# ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQEL +# BQADggIBAIEa1t6gqbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF +# 7SaCinEvGN1Ott5s1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrC +# QDifXcigLiV4JZ0qBXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFc +# jGnRuSvExnvPnPp44pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8 +# wWkZus8W8oM3NG6wQSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbF +# KNOt50MAcN7MmJ4ZiQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP +# 4xeR0arAVeOGv6wnLEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VP +# NTwAvb6cKmx5AdzaROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvr +# moI1VygWy2nyMpqy0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2 +# obhDLN9OTH0eaHDAdwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJ +# uEbTbDJ8WC9nR2XlG3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIGrjCCBJag +# AwIBAgIQBzY3tyRUfNhHrP0oZipeWzANBgkqhkiG9w0BAQsFADBiMQswCQYDVQQG +# EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +# cnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjIw +# MzIzMDAwMDAwWhcNMzcwMzIyMjM1OTU5WjBjMQswCQYDVQQGEwJVUzEXMBUGA1UE +# ChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQg +# UlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMIICIjANBgkqhkiG9w0BAQEF +# AAOCAg8AMIICCgKCAgEAxoY1BkmzwT1ySVFVxyUDxPKRN6mXUaHW0oPRnkyibaCw +# zIP5WvYRoUQVQl+kiPNo+n3znIkLf50fng8zH1ATCyZzlm34V6gCff1DtITaEfFz +# sbPuK4CEiiIY3+vaPcQXf6sZKz5C3GeO6lE98NZW1OcoLevTsbV15x8GZY2UKdPZ +# 7Gnf2ZCHRgB720RBidx8ald68Dd5n12sy+iEZLRS8nZH92GDGd1ftFQLIWhuNyG7 +# QKxfst5Kfc71ORJn7w6lY2zkpsUdzTYNXNXmG6jBZHRAp8ByxbpOH7G1WE15/teP +# c5OsLDnipUjW8LAxE6lXKZYnLvWHpo9OdhVVJnCYJn+gGkcgQ+NDY4B7dW4nJZCY +# OjgRs/b2nuY7W+yB3iIU2YIqx5K/oN7jPqJz+ucfWmyU8lKVEStYdEAoq3NDzt9K +# oRxrOMUp88qqlnNCaJ+2RrOdOqPVA+C/8KI8ykLcGEh/FDTP0kyr75s9/g64ZCr6 +# dSgkQe1CvwWcZklSUPRR8zZJTYsg0ixXNXkrqPNFYLwjjVj33GHek/45wPmyMKVM +# 1+mYSlg+0wOI/rOP015LdhJRk8mMDDtbiiKowSYI+RQQEgN9XyO7ZONj4KbhPvbC +# dLI/Hgl27KtdRnXiYKNYCQEoAA6EVO7O6V3IXjASvUaetdN2udIOa5kM0jO0zbEC +# AwEAAaOCAV0wggFZMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLoW2W1N +# hS9zKXaaL3WMaiCPnshvMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P +# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDCDB3BggrBgEFBQcB +# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr +# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 +# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln +# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwIAYDVR0gBBkwFzAI +# BgZngQwBBAIwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQB9WY7Ak7Zv +# mKlEIgF+ZtbYIULhsBguEE0TzzBTzr8Y+8dQXeJLKftwig2qKWn8acHPHQfpPmDI +# 2AvlXFvXbYf6hCAlNDFnzbYSlm/EUExiHQwIgqgWvalWzxVzjQEiJc6VaT9Hd/ty +# dBTX/6tPiix6q4XNQ1/tYLaqT5Fmniye4Iqs5f2MvGQmh2ySvZ180HAKfO+ovHVP +# ulr3qRCyXen/KFSJ8NWKcXZl2szwcqMj+sAngkSumScbqyQeJsG33irr9p6xeZmB +# o1aGqwpFyd/EjaDnmPv7pp1yr8THwcFqcdnGE4AJxLafzYeHJLtPo0m5d2aR8XKc +# 6UsCUqc3fpNTrDsdCEkPlM05et3/JWOZJyw9P2un8WbDQc1PtkCbISFA0LcTJM3c +# HXg65J6t5TRxktcma+Q4c6umAU+9Pzt4rUyt+8SVe+0KXzM5h0F4ejjpnOHdI/0d +# KNPH+ejxmF/7K9h+8kaddSweJywm228Vex4Ziza4k9Tm8heZWcpw8De/mADfIBZP +# J/tgZxahZrrdVcA6KYawmKAr7ZVBtzrVFZgxtGIJDwq9gdkT/r+k0fNX2bwE+oLe +# Mt8EifAAzV3C+dAjfwAL5HYCJtnwZXZCpimHCUcr5n8apIUP/JiW9lVUKx+A+sDy +# Divl1vupL0QVSucTDh3bNzgaoSv27dZ8/DCCBY0wggR1oAMCAQICEA6bGI750C3n +# 79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoT +# DERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UE +# AxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoX +# DTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0 +# IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNl +# cnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +# AgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQw +# H/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6 +# dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXG +# XuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXn +# Mcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy +# 19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFY +# F/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+Skjqe +# PdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFg +# qrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJR +# R3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7Gr +# hotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2 +# MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9P +# MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIB +# hjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj +# ZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t +# L0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRo +# dHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu +# Y3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV +# 5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8t +# tzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKh +# SLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO +# 7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WY +# IsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3 +# AamfV6peKOK5lDGCA3YwggNyAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoT +# DkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJT +# QTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQQIQBUSv85SdCDmmv9s/X+VhFjAN +# BglghkgBZQMEAgEFAKCB0TAaBgkqhkiG9w0BCQMxDQYLKoZIhvcNAQkQAQQwHAYJ +# KoZIhvcNAQkFMQ8XDTI0MDkwNjEwNDQwN1owKwYLKoZIhvcNAQkQAgwxHDAaMBgw +# FgQUZvArMsLCyQ+CXc6qisnGTxmcz0AwLwYJKoZIhvcNAQkEMSIEIPa9CN4HYXiD +# PGSJaSDiY2zvBaJQuLRp5sP25iB54llNMDcGCyqGSIb3DQEJEAIvMSgwJjAkMCIE +# INL25G3tdCLM0dRAV2hBNm+CitpVmq4zFq9NGprUDHgoMA0GCSqGSIb3DQEBAQUA +# BIICAIIs8VqFXlTsMrevwqkuKL0EcHh4JS3wPWJSFLhf0dBGWEL2CYudnTBv89bF +# o/ctDzk+6Sx6LBq5Kx7iJ3mTKIMHWkVMDHrzGEA51+3Ih+RZbM1CbQccdY/a6IWA +# 1n3zaxvsz/CK6AuFKa3ugGdd6REsu+M4VUwxB31viMz42jN6qAU5zZgqtooWb7L+ +# Nfp0KamFsWLWDEhqBi/zx2FgYVNla+QlySTenNjaw1RwuTTWId6YhZ+++enR1Abj +# x+cByqdvr7aCUcvqiaDYPa/g30JvT9OBiIR4q99HzzLtw4QrufVqS5UejAKPZjZI +# T1kvAnal0EgcA5Zcxf9+HDdhVJJ3lV3DUu5RoiGeZHmF5Ld+3un46sFp1tNk485u +# Oi62s5xbnnbCx/ha9J4nemI4UZDZ6l4uDK2EtKLefnCBZUsyFH9f7N2qAMQjN7DO +# TqB7MPifqKikzCJ6GtceoRpYikaQ+Yiv6tnpuJAuXbf/Ra2KkfZur6i8MB3zTf+7 +# 5+QWXr72TWBQa0xG3UqwsVLAvf5gvNVGdGXTQiilIEArQ91L7VrYir8E3sfSL2XF +# kvhIMsDAX2mCLW87NVtsxgM71EzvZp/fDpJaUX+HNfQWCPZ596kpdGw+ny/XODDs +# /Lh9fr7Szhvo456e8uZ8RWMQNvQlqitznzNTu0lDYS84jsQc +# SIG # End signature block