Spaces:
Paused
Paused
| """ | |
| 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 = '<link rel="stylesheet" href="/harshitpdf-liquidglass.css">' | |
| if "harshitpdf-liquidglass.css" not in content: | |
| content = content.replace("</head>", css_link + "</head>") | |
| 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", | |
| '<svg width="140" height="26" viewBox="0 0 140 26" fill="none" ' | |
| 'xmlns="http://www.w3.org/2000/svg">' | |
| '<path d="M3 2h12l4 4v18a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z" ' | |
| 'fill="#6366f1" opacity="0.9"/>' | |
| '<path d="M15 2l4 4h-4V2z" fill="#818cf8"/>' | |
| '<text x="6" y="17" font-family="system-ui,sans-serif" font-size="7" ' | |
| 'font-weight="700" fill="#fff">PDF</text>' | |
| '<text x="24" y="18" font-family="system-ui,-apple-system,sans-serif" ' | |
| 'font-size="14" font-weight="700" fill="#1a1a2e" letter-spacing="-0.3">' | |
| 'HarshIt<tspan fill="#6366f1">PDF</tspan></text></svg>', | |
| ), | |
| ( | |
| "favicon.svg", | |
| '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">' | |
| '<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1">' | |
| '<stop offset="0" stop-color="#6366f1"/>' | |
| '<stop offset="1" stop-color="#a855f7"/>' | |
| "</linearGradient></defs>" | |
| '<rect width="512" height="512" rx="100" fill="url(#g)"/>' | |
| '<path d="M140 120h160l60 60v200a20 20 0 0 1-20 20H140a20 20 0 0 1-20-20' | |
| 'V140a20 20 0 0 1 20-20z" fill="#fff" opacity="0.95"/>' | |
| '<path d="M300 120l60 60h-60V120z" fill="#ddd" opacity="0.8"/>' | |
| '<text x="170" y="280" font-family="system-ui,sans-serif" font-size="80" ' | |
| 'font-weight="800" fill="#6366f1">PDF</text></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 | |
| 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""" | |
| <script> | |
| (function(){{ | |
| const check = () => {{ | |
| fetch("{proxy_url}login", {{mode: "no-cors"}}) | |
| .then(() => window.location.href = "{proxy_url}") | |
| .catch(() => setTimeout(check, 3000)); | |
| }}; | |
| setTimeout(check, 2000); | |
| }})(); | |
| </script> | |
| <div style="text-align:center;padding-top:20vh;color:#e0e0ff;background:#0f0f23;min-height:100vh;font-family:sans-serif;"> | |
| <h1 style="font-size:2.5rem;background:linear-gradient(135deg,#6366f1,#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">HarshItPDF</h1> | |
| <p style="opacity:0.7;">Your private PDF powerhouse</p> | |
| <div style="width:40px;height:40px;border:3px solid rgba(99,102,241,0.3);border-top-color:#6366f1;border-radius:50%;animation:spin 1s linear infinite;margin:1rem auto;"></div> | |
| <p style="font-family:monospace;font-size:0.9rem;opacity:0.6;margin-top:1rem;">Starting backend... This may take 1β2 min on cold start.</p> | |
| <p style="font-size:0.85rem;opacity:0.5;margin-top:0.5rem;">Default login: admin / changeme123</p> | |
| <style>@keyframes spin{{to{{transform:rotate(360deg)}}}}</style> | |
| </div> | |
| """) | |
| return demo | |
| # Start services in background thread when module is imported | |
| _svc_thread = threading.Thread(target=_start_services, daemon=True) | |
| _svc_thread.start() | |
| # HF Spaces Gradio SDK requires `demo` at module scope | |
| demo = build_gradio_ui() | |