diff --git a/.gitignore b/.gitignore index bba79ab443..4fa993dd01 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,5 @@ times.txt # CodeQL and build artifacts _codeql_detected_source_root install-sh +venv/ +venv/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..435703c51d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Use an official lightweight Python image. +FROM python:3.12-slim + +# Allow statements and log messages to immediately appear in the Knative logs +ENV PYTHONUNBUFFERED True + +# Install system dependencies including Tesseract OCR and languages +RUN apt-get update && apt-get install -y \ + tesseract-ocr \ + tesseract-ocr-eng \ + tesseract-ocr-hin \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory to /app +WORKDIR /app + +# Copy the requirements file and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code +COPY . . + +# Run the web service on container startup +CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app diff --git a/app.py b/app.py new file mode 100644 index 0000000000..cea9ad63b0 --- /dev/null +++ b/app.py @@ -0,0 +1,95 @@ +import os +from flask import Flask, render_template, request, flash, redirect, url_for +from werkzeug.utils import secure_filename +import pytesseract +from PIL import Image, ImageEnhance, ImageFilter + +app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', 'default_dev_secret_key_12345') + +# Setup upload folder +UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__name__)), 'uploads') +os.makedirs(UPLOAD_FOLDER, exist_ok=True) +app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER + +# Ensure allowable file types +ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'tiff'} + +def allowed_file(filename): + return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS + +def preprocess_image(image_path): + """ + Preprocess the image to enhance handwriting recognition. + """ + img = Image.open(image_path) + # Convert to grayscale + img = img.convert('L') + + # Enhance contrast + enhancer = ImageEnhance.Contrast(img) + img = enhancer.enhance(2.0) + + # Apply a slight median filter to remove noise + img = img.filter(ImageFilter.MedianFilter(size=3)) + + return img + +@app.route('/', methods=['GET', 'POST']) +def index(): + extracted_text = None + error = None + + if request.method == 'POST': + # check if the post request has the file part + if 'file' not in request.files: + flash('No file part') + return redirect(request.url) + file = request.files['file'] + + # If the user does not select a file, the browser submits an + # empty file without a filename. + if file.filename == '': + flash('No selected file') + return redirect(request.url) + + if file and allowed_file(file.filename): + filename = secure_filename(file.filename) + filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(filepath) + + # Retrieve parameters for OCR + lang = request.form.get('lang', 'eng') + ocr_mode = request.form.get('mode', 'printed') + + try: + # Preprocess image + img = preprocess_image(filepath) + + # Perform OCR + if ocr_mode == 'handwriting': + # Custom configuration to help with handwriting: + # --psm 6 (Assume a single uniform block of text) + # --oem 1 (Neural nets LSTM only - better for handwriting) + custom_config = r'--oem 1 --psm 6' + else: + # Default for printed text + custom_config = r'--oem 3 --psm 3' + + extracted_text = pytesseract.image_to_string(img, lang=lang, config=custom_config) + except Exception as e: + error = f"Error during OCR processing: {e}" + finally: + # Clean up the file + if os.path.exists(filepath): + os.remove(filepath) + else: + flash('Invalid file type. Allowed: png, jpg, jpeg, gif, webp, bmp, tiff') + return redirect(request.url) + + return render_template('index.html', extracted_text=extracted_text, error=error) + +if __name__ == '__main__': + # Determine port for Render + port = int(os.environ.get('PORT', 5000)) + app.run(host='0.0.0.0', port=port, debug=False) \ No newline at end of file diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000000..ce4018b1db --- /dev/null +++ b/render.yaml @@ -0,0 +1,9 @@ +services: + - type: web + name: tesseract-ocr-web + env: python + buildCommand: "pip install -r requirements.txt" + startCommand: "gunicorn app:app" + envVars: + - key: PYTHON_VERSION + value: 3.12.3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..1611e2a63e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.1.0 +pytesseract==0.3.13 +Pillow==11.1.0 +gunicorn==23.0.0 diff --git a/src/ccutil/helpers.h b/src/ccutil/helpers.h index fa2f38ec83..6cd26ef32b 100644 --- a/src/ccutil/helpers.h +++ b/src/ccutil/helpers.h @@ -198,11 +198,32 @@ inline int IntCastRounded(float x) { inline void ReverseN(void *ptr, int num_bytes) { assert(num_bytes == 1 || num_bytes == 2 || num_bytes == 4 || num_bytes == 8); char *cptr = static_cast(ptr); - int halfsize = num_bytes / 2; - for (int i = 0; i < halfsize; ++i) { - char tmp = cptr[i]; - cptr[i] = cptr[num_bytes - 1 - i]; - cptr[num_bytes - 1 - i] = tmp; + switch (num_bytes) { + case 2: { + char tmp = cptr[0]; + cptr[0] = cptr[1]; + cptr[1] = tmp; + break; + } + case 4: { + char tmp = cptr[0]; + cptr[0] = cptr[3]; + cptr[3] = tmp; + tmp = cptr[1]; + cptr[1] = cptr[2]; + cptr[2] = tmp; + break; + } + case 8: { + for (int i = 0; i < 4; ++i) { + char tmp = cptr[i]; + cptr[i] = cptr[7 - i]; + cptr[7 - i] = tmp; + } + break; + } + default: + break; } } diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000000..88560e94e3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,122 @@ + + + + + + Tesseract OCR Web Interface + + + + +
+ {% with messages = get_flashed_messages() %} + {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% if error %} +
+ {{ error }} +
+ {% endif %} + +
+

Tesseract OCR: Upload Image for Text Extraction

+
+ + + + + + + + + + +
+
+ + {% if extracted_text %} +
+

Extracted Text:

+
{{ extracted_text }}
+
+ {% endif %} + +
+ + + \ No newline at end of file