""" HarshItPDF — HF Spaces Gradio deployment (Free Tier). Build-time (packages.txt): installs Java 17 JDK, Tesseract, unzip, wget. Runtime: downloads JAR, patches bytecode, starts mock license + Java backend. Gradio UI: splash screen with JS auto-redirect to /proxy/{BACKEND_PORT}/ """ import os import sys import time import json import shutil import struct import glob import re import threading import subprocess import urllib.request import zipfile import logging logging.basicConfig( level=logging.INFO, format="[%(name)s] %(levelname)s %(message)s", ) logger = logging.getLogger("harshitpdf") # ═══════════════════════════════════════════════════════════════ # Configuration # ═══════════════════════════════════════════════════════════════ PUBLIC_PORT = int(os.environ.get("PORT", "7860")) BACKEND_PORT = int(os.environ.get("BACKEND_PORT", "7870")) LICENCE_SERVER_PORT = int(os.environ.get("LICENCE_SERVER_PORT", "7871")) RELEASE_TAG = "v2.11.0" JAR_URL = ( f"https://github.com/Stirling-Tools/Stirling-PDF/releases/download/" f"{RELEASE_TAG}/Stirling-PDF-with-login.jar" ) JAR_PATH = "Stirling-PDF-with-login.jar" EXTRACT_DIR = "jar-extracted" STATIC_DIR = os.path.join(EXTRACT_DIR, "BOOT-INF", "classes", "static") ASSETS_DIR = os.path.join(STATIC_DIR, "assets") CLASSES_DIR = os.path.join(EXTRACT_DIR, "BOOT-INF", "classes") LIB_DIR = os.path.join(EXTRACT_DIR, "BOOT-INF", "lib") CONFIGS_DIR = "configs" PATCHED_FLAG = os.path.join(EXTRACT_DIR, ".harshitpdf-patched-v9") BRAND_REPLACEMENTS = [ ("Stirling PDF", "HarshItPDF"), ("Stirling-PDF", "HarshItPDF"), ("StirlingPDF", "HarshItPDF"), ("stirling-pdf", "harshitpdf"), ("stirlingpdf", "harshitpdf"), ("Stirling Tools", "HarshIt"), ("Stirling-Tools", "HarshIt"), ("Stirling Tools ", "HarshIt "), ] # ═══════════════════════════════════════════════════════════════ # Branding helper # ═══════════════════════════════════════════════════════════════ def _brand(text): for old, new in BRAND_REPLACEMENTS: text = text.replace(old, new) return text # ═══════════════════════════════════════════════════════════════ # Import mock license server # ═══════════════════════════════════════════════════════════════ SELF_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, SELF_DIR) from license_server import start_license_server # ═══════════════════════════════════════════════════════════════ # JAR lifecycle # ═══════════════════════════════════════════════════════════════ def _download_jar(): if os.path.exists(JAR_PATH): return logger.info(f"Downloading {JAR_URL} ...") urllib.request.urlretrieve(JAR_URL, JAR_PATH) logger.info("Download complete.") def _extract_jar(): if os.path.exists(os.path.join(EXTRACT_DIR, "BOOT-INF")): if not os.path.exists(PATCHED_FLAG): logger.info("Patch version changed — re-extracting JAR...") shutil.rmtree(EXTRACT_DIR, ignore_errors=True) else: return logger.info("Extracting JAR...") try: subprocess.run( ["unzip", "-q", "-o", JAR_PATH, "-d", EXTRACT_DIR], check=True, timeout=120, ) except (FileNotFoundError, subprocess.TimeoutExpired): with zipfile.ZipFile(JAR_PATH) as zf: zf.extractall(EXTRACT_DIR) logger.info("Extraction complete.") def _get_main_class(): with zipfile.ZipFile(JAR_PATH) as zf: manifest = zf.read("META-INF/MANIFEST.MF").decode() for line in manifest.split("\n"): s = line.strip() if s.startswith("Main-Class:"): return s.split(":", 1)[1].strip() return "org.springframework.boot.loader.launch.JarLauncher" # ═══════════════════════════════════════════════════════════════ # Constant-pool parser (robust) # ═══════════════════════════════════════════════════════════════ CP_TAG_SIZES = { 1: None, 2: None, 3: 4, 4: 4, 5: 8, 6: 8, 7: 2, 8: 2, 16: 2, 19: 2, 20: 2, 9: 4, 10: 4, 11: 4, 12: 4, 17: 4, 18: 4, 15: 3, } def _parse_constant_pool(class_data): if len(class_data) < 10: return [], 10 cp_count = struct.unpack(">H", class_data[8:10])[0] idx = 10 cp = [None] i = 1 while i < cp_count: if idx >= len(class_data): break tag = class_data[idx] idx += 1 if tag == 1 or tag == 2: if idx + 2 > len(class_data): break length = struct.unpack(">H", class_data[idx:idx+2])[0] if idx + 2 + length > len(class_data): break val = class_data[idx+2:idx+2+length].decode("utf-8", errors="replace") cp.append((tag, val)) idx += 2 + length i += 1 elif tag in (5, 6): if idx + 8 > len(class_data): break cp.append((tag, None)) cp.append((tag, None)) idx += 8 i += 2 elif tag in CP_TAG_SIZES and CP_TAG_SIZES[tag] is not None: size = CP_TAG_SIZES[tag] if idx + size > len(class_data): break if tag in (7, 8, 16, 19, 20): val = struct.unpack(">H", class_data[idx:idx+2])[0] cp.append((tag, val)) elif tag in (9, 10, 11, 12, 17, 18): a = struct.unpack(">H", class_data[idx:idx+2])[0] b = struct.unpack(">H", class_data[idx+2:idx+4])[0] cp.append((tag, (a, b))) elif tag == 15: cp.append((tag, (class_data[idx], struct.unpack(">H", class_data[idx+1:idx+3])[0]))) else: cp.append((tag, None)) idx += size i += 1 else: cp.append((tag, None)) break return cp, idx def _get_method_code_ranges(class_data): _, idx = _parse_constant_pool(class_data) if not idx: return [] idx += 6 if idx + 2 > len(class_data): return [] interfaces_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 + interfaces_count * 2 if idx + 2 > len(class_data): return [] fields_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 for _ in range(fields_count): if idx + 6 > len(class_data): return [] idx += 6 attr_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 for _ in range(attr_count): if idx + 6 > len(class_data): return [] idx += 2 attr_len = struct.unpack(">I", class_data[idx:idx+4])[0] idx += 4 + attr_len if idx + 2 > len(class_data): return [] methods_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 code_ranges = [] for _ in range(methods_count): if idx + 6 > len(class_data): break idx += 6 attr_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 for _ in range(attr_count): if idx + 6 > len(class_data): break idx += 2 attr_len = struct.unpack(">I", class_data[idx:idx+4])[0] idx += 4 attr_start = idx attr_end = idx + attr_len if attr_end <= len(class_data) and attr_len >= 8: code_len = struct.unpack(">I", class_data[attr_start+4:attr_start+8])[0] code_start = attr_start + 8 code_end = code_start + code_len if code_end <= attr_end: code_ranges.append((code_start, code_end)) idx = attr_end return code_ranges def _get_method_code_info(class_data): cp, idx = _parse_constant_pool(class_data) if not idx: return [] idx += 6 if idx + 2 > len(class_data): return [] interfaces_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 + interfaces_count * 2 if idx + 2 > len(class_data): return [] fields_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 for _ in range(fields_count): if idx + 6 > len(class_data): return [] idx += 6 attr_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 for _ in range(attr_count): if idx + 6 > len(class_data): return [] idx += 2 attr_len = struct.unpack(">I", class_data[idx:idx+4])[0] idx += 4 + attr_len if idx + 2 > len(class_data): return [] methods_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 methods = [] for _ in range(methods_count): if idx + 6 > len(class_data): break name_idx = struct.unpack(">H", class_data[idx+2:idx+4])[0] idx += 6 attr_count = struct.unpack(">H", class_data[idx:idx+2])[0] idx += 2 name = "" if 0 < name_idx < len(cp) and cp[name_idx] and cp[name_idx][0] == 1: name = cp[name_idx][1] for _ in range(attr_count): if idx + 6 > len(class_data): break attr_name_idx = struct.unpack(">H", class_data[idx:idx+2])[0] attr_len = struct.unpack(">I", class_data[idx+2:idx+6])[0] idx += 6 attr_start = idx attr_end = idx + attr_len if 0 < attr_name_idx < len(cp) and cp[attr_name_idx] and cp[attr_name_idx][0] == 1: attr_name = cp[attr_name_idx][1] if attr_name == "Code" and attr_len >= 8: code_len = struct.unpack(">I", class_data[attr_start+4:attr_start+8])[0] code_start = attr_start + 8 code_end = code_start + code_len if code_end <= attr_end: methods.append((name, code_start, code_end)) idx = attr_end return methods def _find_fieldref_index(class_data, class_name_substr, field_name): cp, _ = _parse_constant_pool(class_data) if not cp: return None field_name_idx = None for i, entry in enumerate(cp): if entry and entry[0] == 1 and entry[1] == field_name: field_name_idx = i break if not field_name_idx: return None nat_idx = None for i, entry in enumerate(cp): if entry and entry[0] == 12 and isinstance(entry[1], tuple) and entry[1][0] == field_name_idx: nat_idx = i break if not nat_idx: return None class_idx = None for i, entry in enumerate(cp): if entry and entry[0] == 7 and isinstance(entry[1], int): cn_entry = cp[entry[1]] if cn_entry and cn_entry[0] == 1 and class_name_substr in cn_entry[1]: class_idx = i break if not class_idx: return None for i, entry in enumerate(cp): if entry and entry[0] == 9 and isinstance(entry[1], tuple) and entry[1][0] == class_idx and entry[1][1] == nat_idx: return i return None # ═══════════════════════════════════════════════════════════════ # Bytecode patching # ═══════════════════════════════════════════════════════════════ def _patch_method_to_return_true(class_data, class_name=""): data = bytearray(class_data) ranges = _get_method_code_ranges(class_data) modified = False for start, end in ranges: i = start while i < end - 1: if data[i] == 0x03 and data[i + 1] == 0xac: data[i] = 0x04 modified = True i += 2 continue if data[i] == 0x04 and data[i + 1] == 0xac: i += 2 continue i += 1 return modified, bytes(data) def _patch_class_file(path): with open(path, "rb") as fh: data = fh.read() modified, new_data = _patch_method_to_return_true(data, os.path.basename(path)) if modified: with open(path, "wb") as fh: fh.write(new_data) return True return False def _is_license_class(filename): name = filename.lower() return any(w in name for w in [ "license", "licenseverifier", "licensechecker", "dynamiclicense", "premiumenabled", ]) def _is_license_jar(filename): name = filename.lower() return "proprietary" in name or "license" in name or "ee" in name def _patch_license_key_checker(path): """Patch getPremiumLicenseEnabledResult() → ENTERPRISE robustly.""" with open(path, "rb") as fh: data = bytearray(fh.read()) enterprise_idx = _find_fieldref_index(data, "License", "ENTERPRISE") if enterprise_idx: methods = _get_method_code_info(data) for name, cstart, cend in methods: if name == "getPremiumLicenseEnabledResult": for i in range(cstart, cend - 4): if data[i] == 0x2a and data[i+1] == 0xb4 and data[i+4] == 0xb0: data[i] = 0xb2 data[i+1] = (enterprise_idx >> 8) & 0xff data[i+2] = enterprise_idx & 0xff data[i+3] = 0x00 data[i+4] = 0xb0 with open(path, "wb") as fh: fh.write(bytes(data)) logger.info(f"Robust-patched {name} → ENTERPRISE (idx={enterprise_idx})") return True logger.warning(f"Found {name} but no getfield+areturn pattern") return False logger.warning("ENTERPRISE not found in constant pool, trying hardcoded fallback...") if len(data) > 5643: expected = bytes([0xb4, 0x00, 0x0d, 0xb0]) if data[5638:5642] == expected: data[5638:5643] = bytes([0xb2, 0x00, 0x4f, 0x00, 0xb0]) with open(path, "wb") as fh: fh.write(bytes(data)) logger.info("Patched LicenseKeyChecker using hardcoded offset fallback") return True logger.error("Could not patch LicenseKeyChecker") return False def _extract_and_patch_license_classes_from_jar(jar_path): patched_files = [] with zipfile.ZipFile(jar_path, "r") as zf: for name in zf.namelist(): if not name.endswith(".class"): continue if not _is_license_class(os.path.basename(name)): continue data = zf.read(name) mod1, d1 = _patch_method_to_return_true(data, os.path.basename(name)) if mod1: dest_path = os.path.join(CLASSES_DIR, name) os.makedirs(os.path.dirname(dest_path), exist_ok=True) with open(dest_path, "wb") as fh: fh.write(d1) patched_files.append(name) if patched_files: logger.info(f"Extracted/patched from {os.path.basename(jar_path)}: {len(patched_files)} classes") return len(patched_files) def _patch_license_classes(): patched = 0 if os.path.exists(CLASSES_DIR): for root, dirs, files in os.walk(CLASSES_DIR): for f in files: if not f.endswith(".class") or not _is_license_class(f): continue path = os.path.join(root, f) try: if _patch_class_file(path): patched += 1 logger.info(f"Patched license class: {f}") except Exception as e: logger.warning(f"Could not patch {f}: {e}") if os.path.exists(LIB_DIR): for f in os.listdir(LIB_DIR): if not f.endswith(".jar") or not _is_license_jar(f): continue try: patched += _extract_and_patch_license_classes_from_jar(os.path.join(LIB_DIR, f)) except Exception as e: logger.warning(f"Could not process {f}: {e}") lck_path = os.path.join( CLASSES_DIR, "stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.class" ) if not os.path.exists(lck_path) and os.path.exists(LIB_DIR): for jf in os.listdir(LIB_DIR): if "proprietary" not in jf.lower(): continue with zipfile.ZipFile(os.path.join(LIB_DIR, jf)) as zf: cp = "stirling/software/proprietary/security/configuration/ee/LicenseKeyChecker.class" if cp in zf.namelist(): os.makedirs(os.path.dirname(lck_path), exist_ok=True) with open(lck_path, "wb") as fh: fh.write(zf.read(cp)) logger.info("Extracted LicenseKeyChecker.class for patching") break if os.path.exists(lck_path): try: if _patch_license_key_checker(lck_path): patched += 1 except Exception as e: logger.warning(f"Could not patch LicenseKeyChecker: {e}") else: logger.warning("LicenseKeyChecker.class not found") if patched: logger.info(f"Patched {patched} license verifier classes total.") # ═══════════════════════════════════════════════════════════════ # HTML / JS / SVG / CSS patching # ═══════════════════════════════════════════════════════════════ def _patch_html(): count = 0 if not os.path.exists(STATIC_DIR): return for root, dirs, files in os.walk(STATIC_DIR): for f in files: if not f.endswith(".html"): continue path = os.path.join(root, f) with open(path, "r", encoding="utf-8", errors="replace") as fh: content = fh.read() content = _brand(content) content = content.replace( "The Free Adobe Acrobat alternative (10M+ Downloads)", "Your private PDF powerhouse by HarshIt", ) css_link = '' if "harshitpdf-liquidglass.css" not in content: content = content.replace("", css_link + "") with open(path, "w", encoding="utf-8") as fh: fh.write(content) count += 1 logger.info(f"Patched {count} HTML files.") def _patch_js(): count = 0 if not os.path.exists(ASSETS_DIR): return for f in os.listdir(ASSETS_DIR): if not f.endswith(".js"): continue path = os.path.join(ASSETS_DIR, f) with open(path, "r", encoding="utf-8", errors="replace") as fh: content = fh.read() orig = content content = _brand(content) content = re.sub( r'requiresPremium\s*[=:]=?\s*(?:!0|!1|true|false|null)', 'requiresPremium=!1', content, ) content = re.sub( r'["\'](?:Premium feature[:\s].*?|Enterprise only[:\s].*?|Enterprise Seats|Upgrade to Server Plan)["\']', '""', content, ) content = re.sub( r'premiumEnabled\s*[=:]=?\s*[se][?\w]+\.premiumEnabled\?\??[!\d]+', 'premiumEnabled:!0', content, ) if content != orig: with open(path, "w", encoding="utf-8") as fh: fh.write(content) count += 1 logger.info(f"Patched {count} JS files.") def _write_svgs(): LOGO_SVGS = [ ( "StirlingPDFLogoBlackText.svg", '', ), ( "favicon.svg", '', ), ] logo_dir = os.path.join(STATIC_DIR, "modern-logo") os.makedirs(logo_dir, exist_ok=True) for name, svg in LOGO_SVGS: with open(os.path.join(logo_dir, name), "w") as fh: fh.write(svg) logger.info("Wrote SVGs.") def _write_manifests(): manifest = { "short_name": "HarshItPDF", "name": "HarshItPDF", "icons": [], "start_url": ".", "display": "standalone", "theme_color": "#0a0a1a", "background_color": "#0a0a1a", } for name in ("manifest.json", "manifest-classic.json"): path = os.path.join(STATIC_DIR, name) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as fh: json.dump(manifest, fh, indent=2) def _write_css(): candidates = glob.glob(os.path.join(SELF_DIR, "liquidglass*.css")) css_src = candidates[0] if candidates else None css_dst = os.path.join(STATIC_DIR, "harshitpdf-liquidglass.css") if css_src and os.path.exists(css_src): shutil.copy(css_src, css_dst) logger.info(f"Copied CSS: {css_src} -> {css_dst}") else: with open(css_dst, "w") as fh: fh.write("/* HarshItPDF Liquid Glass fallback */\n") def _patch_settings_template(): template_path = os.path.join(CLASSES_DIR, "settings.yml.template") if not os.path.exists(template_path): logger.warning("settings.yml.template not found.") return with open(template_path, "r") as fh: content = fh.read() content = _brand(content) with open(template_path, "w") as fh: fh.write(content) logger.info("Patched settings.yml.template.") def _write_settings_yml(): os.makedirs(CONFIGS_DIR, exist_ok=True) settings_path = os.path.join(CONFIGS_DIR, "settings.yml") template_path = os.path.join(CLASSES_DIR, "settings.yml.template") if os.path.exists(template_path): with open(template_path, "r") as fh: content = fh.read() else: content = """# HarshItPDF Settings security: enableLogin: true initialLogin: username: admin password: changeme123 ui: appName: HarshItPDF appNavbarName: HarshItPDF homeDescription: Your private PDF powerhouse by HarshIt system: showUpdate: false rootUriPath: "" defaultLocale: en-US premium: enabled: false enterpriseEdition: enabled: true maxUsers: 999 endpoints: groupsToRemove: [] """ content = _brand(content) with open(settings_path, "w") as fh: fh.write(content) logger.info(f"Wrote settings.yml ({len(content)} chars).") def _patch_banner(): banner_path = os.path.join(CLASSES_DIR, "banner.txt") os.makedirs(os.path.dirname(banner_path), exist_ok=True) with open(banner_path, "w") as fh: fh.write(r""" _ __ _ _ ___ ___ ___ ___ | |/ /__ _ _ __| |_ |_ _| _ \ _ _| __| _ \ | ' // _` | '_ \ ' \ | || _/| || _|| / | . \ (_| | | | | || | | || | | |_| |_| _| |_|\_\__,_|_| |_| |__/ Powered by Spring Boot ${spring-boot.version} """) def _patch_meta(): for name in ("og-metadata.json",): path = os.path.join(STATIC_DIR, name) if os.path.exists(path): with open(path, "r") as fh: content = fh.read() with open(path, "w") as fh: fh.write(_brand(content)) def _fix_python_path(): venv_dir = "/opt/venv" venv_bin = os.path.join(venv_dir, "bin") venv_python = os.path.join(venv_bin, "python3") if not os.path.exists(venv_dir): os.makedirs(venv_bin, exist_ok=True) system_python = shutil.which("python3") if system_python and not os.path.exists(venv_python): try: os.symlink(system_python, venv_python) except Exception as e: logger.warning(f"Could not create python3 symlink: {e}") def _install_opencv(): try: import cv2 logger.info(f"OpenCV available: {cv2.__version__}") except ImportError: logger.info("OpenCV not found, installing...") try: subprocess.run( [sys.executable, "-m", "pip", "install", "--quiet", "opencv-python-headless"], check=False, timeout=60, ) import cv2 logger.info(f"OpenCV installed: {cv2.__version__}") except Exception as e: logger.warning(f"Could not install OpenCV: {e}") def _compile_license_override(): java_src = """package stirling.software.SPDF.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @Configuration public class HarshItLicenseOverride { @Bean @ConditionalOnMissingBean(name = "runningProOrHigher") @Qualifier("runningProOrHigher") public boolean runningProOrHigher() { return true; } } """ src_path = os.path.join(EXTRACT_DIR, "HarshItLicenseOverride.java") with open(src_path, "w") as fh: fh.write(java_src) lib_dir = os.path.join(EXTRACT_DIR, "BOOT-INF", "lib") if not os.path.exists(lib_dir): logger.warning("BOOT-INF/lib not found, skipping license override compile") return False jars = [os.path.join(lib_dir, j) for j in os.listdir(lib_dir) if j.endswith(".jar")] classpath = ":".join(jars) dest_dir = os.path.join(EXTRACT_DIR, "BOOT-INF", "classes") try: result = subprocess.run( ["javac", "-cp", classpath, "-d", dest_dir, src_path], capture_output=True, text=True, timeout=60 ) if result.returncode == 0: logger.info("Compiled HarshItLicenseOverride.class") return True else: logger.warning(f"javac failed: {result.stderr[:200]}") return False except Exception as e: logger.warning(f"Could not compile override: {e}") return False def _patch_files(): if os.path.exists(PATCHED_FLAG): return logger.info("Applying HarshItPDF patches...") _patch_html() _patch_js() _write_svgs() _write_manifests() _write_css() _patch_settings_template() _write_settings_yml() _patch_banner() _patch_meta() _fix_python_path() _install_opencv() _patch_license_classes() _compile_license_override() with open(PATCHED_FLAG, "w") as fh: fh.write(str(time.time())) logger.info("All patches applied.") # ═══════════════════════════════════════════════════════════════ # Backend lifecycle # ═══════════════════════════════════════════════════════════════ _backend_process = None _backend_ready = threading.Event() def _stream_output(pipe): try: for line in pipe: print(line, end="") if "Started SPDFApplication" in line: _backend_ready.set() except Exception as e: logger.warning(f"Backend stream error: {e}") def _find_tessdata(): candidates = [ "/usr/share/tesseract-ocr/5/tessdata/", "/usr/share/tesseract-ocr/4.00/tessdata/", "/usr/share/tesseract-ocr/4/tessdata/", "/usr/share/tessdata/", ] for c in candidates: if os.path.exists(c): return c return "" def start_backend(): logger.info("Launching HarshItPDF...") main_class = _get_main_class() env = os.environ.copy() env["SERVER_PORT"] = str(BACKEND_PORT) env["SYSTEM_ROOTURIPATH"] = "" env["SECURITY_ENABLELOGIN"] = "true" env["SECURITY_INITIALLOGIN_USERNAME"] = "admin" env["SECURITY_INITIALLOGIN_PASSWORD"] = "changeme123" env["UI_APPNAME"] = "HarshItPDF" env["UI_APPNAVBARNAME"] = "HarshItPDF" env["UI_HOMEDESCRIPTION"] = "Your private PDF powerhouse by HarshIt" env["SYSTEM_SHOWUPDATE"] = "false" env["PREMIUM_ENABLED"] = "true" env["PREMIUM_KEY"] = "harshitpdf-unlocked-0000-0000-0000-000000000001" env["DISABLE_ADDITIONAL_FEATURES"] = "false" env["PYTHON_PATH"] = shutil.which("python3") or "/usr/bin/python3" tessdata = _find_tessdata() if tessdata: env["TESSDATA_PREFIX"] = tessdata logger.info(f"Launching Java backend on internal port {BACKEND_PORT}...") global _backend_process _backend_process = subprocess.Popen( [ "java", "-Xmx2G", "-XX:+UseSerialGC", "-XX:MaxRAMPercentage=60.0", "-cp", EXTRACT_DIR, main_class, ], env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) threading.Thread(target=_stream_output, args=(_backend_process.stdout,), daemon=True).start() time.sleep(2) def check_backend(): try: resp = urllib.request.urlopen(f"http://127.0.0.1:{BACKEND_PORT}/login", timeout=3) return resp.status == 200 except Exception: return False # ═══════════════════════════════════════════════════════════════ # Service orchestrator (runs at module import on HF Spaces) # ═══════════════════════════════════════════════════════════════ def _start_services(): logger.info("=" * 60) logger.info("HarshItPDF Starting...") logger.info("=" * 60) _download_jar() _extract_jar() _patch_files() logger.info(f"Starting mock license server on port {LICENCE_SERVER_PORT}...") lic_thread = threading.Thread(target=start_license_server, daemon=True) lic_thread.start() time.sleep(0.5) start_backend() logger.info("Waiting for backend to be ready...") _backend_ready.wait(timeout=120) if check_backend(): logger.info(f"HarshItPDF backend is live on port {BACKEND_PORT}") else: logger.warning("Backend health check failed — app may still be starting") # Keep-alive monitor while True: if _backend_process and _backend_process.poll() is not None: logger.error("Java backend exited! Restarting...") _backend_ready.clear() start_backend() _backend_ready.wait(timeout=120) time.sleep(10) # ═══════════════════════════════════════════════════════════════ # Gradio UI # ═══════════════════════════════════════════════════════════════ import gradio as gr # ZeroGPU guard (required on GPU-backed spaces, harmless on CPU) try: import spaces @spaces.GPU(duration=1) def _gpuguard(): pass except ImportError: pass def build_gradio_ui(): proxy_url = f"/proxy/{BACKEND_PORT}/" with gr.Blocks(title="HarshItPDF", theme=gr.themes.Base()) as demo: gr.HTML(f"""
Your private PDF powerhouse
Starting backend... This may take 1–2 min on cold start.
Default login: admin / changeme123