Spaces:
Sleeping
Sleeping
| import os | |
| import subprocess | |
| import uuid | |
| from flask import Flask, request, render_template_string, send_file, jsonify | |
| app = Flask(__name__) | |
| # Safe directories using the user's home directory context | |
| UPLOAD_FOLDER = os.path.expanduser('~/uploads') | |
| OUTPUT_FOLDER = os.path.expanduser('~/outputs') | |
| os.makedirs(UPLOAD_FOLDER, exist_ok=True) | |
| os.makedirs(OUTPUT_FOLDER, exist_ok=True) | |
| # Extension mapping matrix defining what can safely turn into what | |
| ALLOWED_CONVERSIONS = { | |
| 'docx': ['pdf', 'odt', 'txt', 'html'], | |
| 'doc': ['pdf', 'docx', 'odt'], | |
| 'xlsx': ['pdf', 'csv', 'ods', 'html'], | |
| 'xls': ['pdf', 'xlsx', 'csv'], | |
| 'pptx': ['pdf', 'odp', 'html'], | |
| 'ppt': ['pdf', 'pptx'], | |
| 'odt': ['pdf', 'docx', 'txt'], | |
| 'ods': ['pdf', 'xlsx', 'csv'], | |
| 'txt': ['pdf', 'docx'], | |
| 'csv': ['xlsx', 'pdf'] | |
| } | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Universal Multi-Format Document Converter</title> | |
| <style> | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #f0f2f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; } | |
| .card { background: white; padding: 2.5rem; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.08); width: 480px; text-align: center; } | |
| h2 { color: #1a1a1a; margin-top: 0; margin-bottom: 0.5rem; font-size: 1.6rem; } | |
| p.subtitle { color: #666; margin-bottom: 2rem; font-size: 0.9rem; } | |
| .drop-zone { border: 2px dashed #4A90E2; padding: 2.5rem 1.5rem; border-radius: 10px; background: #f8faff; cursor: pointer; transition: all 0.2s ease; margin-bottom: 1.5rem; } | |
| .drop-zone:hover { background: #f0f5ff; border-color: #357ABD; } | |
| .drop-zone p { margin: 0; color: #444; font-size: 1rem; font-weight: 500; } | |
| .drop-zone span { display: block; color: #888; font-size: 0.8rem; margin-top: 0.5rem; } | |
| .control-group { text-align: left; margin-bottom: 1.5rem; display: none; } | |
| label { display: block; font-size: 0.85rem; color: #555; margin-bottom: 0.5rem; font-weight: 600; } | |
| select { width: 100%; padding: 0.75rem; border: 1px solid #ccc; border-radius: 6px; font-size: 1rem; background: white; outline: none; } | |
| select:focus { border-color: #4A90E2; } | |
| button { background: #4A90E2; color: white; border: none; padding: 0.85rem; font-size: 1rem; border-radius: 6px; cursor: pointer; width: 100%; font-weight: bold; transition: background 0.2s; } | |
| button:hover { background: #357ABD; } | |
| button:disabled { background: #cbd5e1; cursor: not-allowed; } | |
| #status { margin-top: 1.2rem; font-size: 0.9rem; color: #333; font-weight: 500; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h2>Multi-Format Converter</h2> | |
| <p class="subtitle">Powered by optimized LibreOffice daemon</p> | |
| <div class="drop-zone" id="dropZone"> | |
| <p id="dropText">Drag & drop your document here</p> | |
| <span>Supports DOCX, XLSX, PPTX, ODT, TXT, CSV etc.</span> | |
| <input type="file" id="fileInput" style="display: none;"> | |
| </div> | |
| <div class="control-group" id="targetGroup"> | |
| <label for="targetFormat">SELECT OUTPUT FORMAT</label> | |
| <select id="targetFormat"></select> | |
| </div> | |
| <button id="convertBtn" disabled>Convert Document</button> | |
| <p id="status"></p> | |
| </div> | |
| <script> | |
| const dropZone = document.getElementById('dropZone'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const convertBtn = document.getElementById('convertBtn'); | |
| const targetGroup = document.getElementById('targetGroup'); | |
| const targetFormat = document.getElementById('targetFormat'); | |
| const status = document.getElementById('status'); | |
| const dropText = document.getElementById('dropText'); | |
| let selectedFile = null; | |
| // Allowed format configuration matrix injected straight from server config rules | |
| const map = {{ mapping | tojson }}; | |
| dropZone.onclick = () => fileInput.click(); | |
| fileInput.onchange = (e) => handleFile(e.target.files[0]); | |
| dropZone.ondragover = (e) => { e.preventDefault(); dropZone.style.background = '#eef3f9'; }; | |
| dropZone.ondragleave = () => { dropZone.style.background = '#f8faff'; }; | |
| dropZone.ondrop = (e) => { e.preventDefault(); handleFile(e.dataTransfer.files[0]); }; | |
| function handleFile(file) { | |
| if (!file) return; | |
| const ext = file.name.split('.').pop().toLowerCase(); | |
| if (map[ext]) { | |
| selectedFile = file; | |
| dropText.innerText = `Selected: ${file.name}`; | |
| // Build dynamic downstream selectable dropdown context arrays | |
| targetFormat.innerHTML = ""; | |
| map[ext].forEach(fmt => { | |
| const opt = document.createElement('option'); | |
| opt.value = fmt; | |
| opt.innerText = fmt.toUpperCase(); | |
| targetFormat.appendChild(opt); | |
| }); | |
| targetGroup.style.display = 'block'; | |
| convertBtn.disabled = false; | |
| status.innerText = ""; | |
| } else { | |
| targetGroup.style.display = 'none'; | |
| convertBtn.disabled = true; | |
| status.innerText = `Extension .${ext.toUpperCase()} is not natively mapable.`; | |
| } | |
| } | |
| convertBtn.onclick = async () => { | |
| if (!selectedFile) return; | |
| const formData = new FormData(); | |
| formData.append('file', selectedFile); | |
| formData.append('target', targetFormat.value); | |
| convertBtn.disabled = true; | |
| status.style.color = "#4A90E2"; | |
| status.innerText = "Processing via hot memory stream... Engine active."; | |
| try { | |
| const response = await fetch('/convert', { method: 'POST', body: formData }); | |
| if (!response.ok) throw new Error('Daemon processing execution fault'); | |
| const blob = await response.blob(); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| const baseName = selectedFile.name.substring(0, selectedFile.name.lastIndexOf('.')); | |
| a.download = `${baseName}.${targetFormat.value}`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| status.style.color = "#2e7d32"; | |
| status.innerText = "Success! Converted immediately."; | |
| } catch (err) { | |
| status.style.color = "#d32f2f"; | |
| status.innerText = "Conversion error occurred. Verify data integrity."; | |
| } finally { | |
| convertBtn.disabled = false; | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE, mapping=ALLOWED_CONVERSIONS) | |
| def convert(): | |
| if 'file' not in request.files or 'target' not in request.form: | |
| return jsonify({"error": "Malformed query structure payload"}), 400 | |
| file = request.files['file'] | |
| target_ext = request.form['target'].lower() | |
| if file.filename == '': | |
| return jsonify({"error": "Empty filename structure context"}), 400 | |
| source_ext = file.filename.split('.')[-1].lower() | |
| if source_ext not in ALLOWED_CONVERSIONS or target_ext not in ALLOWED_CONVERSIONS[source_ext]: | |
| return jsonify({"error": "Unsupported cross-format compilation sequence requested"}), 400 | |
| # Ensure thread-safe runtime paths | |
| job_id = str(uuid.uuid4()) | |
| input_filename = f"{job_id}.{source_ext}" | |
| output_filename = f"{job_id}.{target_ext}" | |
| input_path = os.path.join(UPLOAD_FOLDER, input_filename) | |
| output_path = os.path.join(OUTPUT_FOLDER, output_filename) | |
| file.save(input_path) | |
| try: | |
| # We hook straight into the background listening port 2002 via 'unoconvert' | |
| # From app.py line ~160 | |
| subprocess.run([ | |
| 'unoconvert', | |
| '--port', '2002', # Connects to unoserver's main port | |
| '--convert-to', target_ext, | |
| input_path, | |
| output_path | |
| ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| if os.path.exists(output_path): | |
| return send_file(output_path, as_attachment=True) | |
| else: | |
| return jsonify({"error": "Pipeline processing did not generate binary assets"}), 500 | |
| except subprocess.CalledProcessError as e: | |
| return jsonify({"error": "Daemon stream pipeline communications broken"}), 500 | |
| finally: | |
| # Continuous clean execution lifecycle routines | |
| if os.path.exists(input_path): | |
| os.remove(input_path) | |
| if os.path.exists(output_path): | |
| os.remove(output_path) | |
| if __name__ == '__main__': | |
| # Fallback to standard testing configurations locally if needed | |
| app.run(host='0.0.0.0', port=7860) |