from flask import Flask, request, jsonify from flask_cors import CORS import cv2 import numpy as np import base64 import io from pyzbar.pyzbar import decode from PIL import Image import os import logging import requests # ================== INITIALISATION ================== app = Flask(__name__) CORS(app, resources={r"/api/*": {"origins": "*"}}) HF_SPACE = os.environ.get("SPACE_ID") is not None PORT = 7860 if HF_SPACE else int(os.environ.get("PORT", 5000)) logging.basicConfig(level=logging.INFO) logger = logging.getLogger("barcode-api") # ================== UTILS ================== def safe_b64decode(data: str) -> bytes: """Base64 decode safe (Flutter/Web compatible)""" return base64.b64decode(data + "=" * (-len(data) % 4)) def decode_barcode(image_bytes: bytes) -> dict: """Decode barcode using PIL then OpenCV""" try: # --- PIL first --- try: pil_image = Image.open(io.BytesIO(image_bytes)) barcodes = decode(pil_image) if barcodes: b = barcodes[0] return { "success": True, "barcode": b.data.decode("utf-8"), "type": b.type, "method": "pil" } except Exception as e: logger.debug(f"PIL failed: {e}") # --- OpenCV fallback --- try: nparr = np.frombuffer(image_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_GRAYSCALE) if img is not None: barcodes = decode(img) if barcodes: b = barcodes[0] return { "success": True, "barcode": b.data.decode("utf-8"), "type": b.type, "method": "opencv" } # Contrast enhancement clahe = cv2.createCLAHE(2.0, (8, 8)) enhanced = clahe.apply(img) barcodes = decode(enhanced) if barcodes: b = barcodes[0] return { "success": True, "barcode": b.data.decode("utf-8"), "type": b.type, "method": "enhanced" } except Exception as e: logger.debug(f"OpenCV failed: {e}") return {"success": False, "error": "No barcode detected"} except Exception as e: logger.error(e) return {"success": False, "error": str(e)} # ================== FLUTTER API ================== @app.route("/api/decode-barcode", methods=["POST"]) def api_decode_barcode(): if not request.is_json: return jsonify({"success": False, "error": "JSON required"}), 400 data = request.get_json() image_data = data.get("image") if not image_data: return jsonify({"success": False, "error": "Image missing"}), 400 if "," in image_data: image_data = image_data.split(",")[1] if len(image_data) > 3 * 1024 * 1024: return jsonify({"success": False, "error": "Image too large"}), 400 image_bytes = safe_b64decode(image_data) result = decode_barcode(image_bytes) return jsonify(result) @app.route("/api/product-info/", methods=["GET"]) def api_product_info(barcode): local_db = { "3017620422003": { "name": "Nutella", "brand": "Ferrero", "category": "Food", "price": 4.99, "description": "Hazelnut spread" }, "5901234123457": { "name": "Milk UHT", "brand": "Candia", "category": "Food", "price": 1.20, "description": "UHT milk" } } if barcode in local_db: return jsonify({"success": True, "product": local_db[barcode], "source": "local"}) # OpenFoodFacts fallback try: r = requests.get( f"https://world.openfoodfacts.org/api/v0/product/{barcode}.json", timeout=4 ) if r.status_code == 200: data = r.json() if data.get("status") == 1: p = data["product"] return jsonify({ "success": True, "source": "openfoodfacts", "product": { "name": p.get("product_name"), "brand": p.get("brands"), "category": p.get("categories"), "description": p.get("generic_name"), "image": p.get("image_url") } }) except Exception as e: logger.debug(e) return jsonify({"success": False, "error": "Product not found"}), 404 # ================== WEB SCAN ================== @app.route("/api/scan", methods=["POST"]) def api_scan(): if not request.is_json: return jsonify({"success": False, "error": "JSON required"}), 400 image_data = request.json.get("image") if not image_data: return jsonify({"success": False, "error": "Image missing"}), 400 if "," in image_data: image_data = image_data.split(",")[1] image_bytes = safe_b64decode(image_data) return jsonify(decode_barcode(image_bytes)) # ================== HEALTH ================== @app.route("/api/health") def health(): return jsonify({ "status": "ok", "service": "Barcode Scanner API", "platform": "HuggingFace" if HF_SPACE else "Local", "endpoints": [ "/api/decode-barcode", "/api/product-info/", "/api/scan", "/api/health" ] }) @app.route("/") def home(): return jsonify({ "message": "Barcode Scanner API", "use": "/api/decode-barcode (POST)" }) # ================== MAIN ================== if __name__ == "__main__": app.run(host="0.0.0.0", port=PORT, debug=False)