Spaces:
Running
Running
""" | |
Streamlit Universal File‑Format Changer | |
-------------------------------------- | |
A Streamlit app ready for Hugging Face Spaces that **actually converts file | |
contents when possible**, instead of merely renaming extensions. | |
* Image ↔ image conversions via **Pillow** (JPEG, PNG, GIF, BMP, TIFF, ICO, WEBP) | |
* Plain‑text files kept intact but re‑encoded (UTF‑8) when changing among | |
text‑like extensions (txt, md, csv, json, xml, html, css, js) | |
* Disallowed uploads: `.exe`, `.bin` | |
* Everything is bundled into one ZIP download. | |
Created 2025‑05‑22 • v2 | |
""" | |
from __future__ import annotations | |
# NOTE: Set env vars *before* importing Streamlit ------------------------------ | |
import os, pathlib | |
os.environ.setdefault("STREAMLIT_HOME", "/tmp/.streamlit") | |
os.environ.setdefault("HOME", "/tmp") | |
pathlib.Path(os.environ["STREAMLIT_HOME"]).mkdir(parents=True, exist_ok=True) | |
import io | |
import zipfile | |
from datetime import datetime | |
from pathlib import Path | |
import streamlit as st | |
from PIL import Image # Pillow for real image conversion | |
# ----------------------------------------------------------------------------- | |
# Supported extensions --------------------------------------------------------- | |
# ----------------------------------------------------------------------------- | |
IMAGE_EXTS = { | |
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".ico", ".webp", | |
} | |
TEXT_EXTS = { | |
".txt", ".md", ".csv", ".json", ".xml", ".html", ".css", ".js", | |
} | |
ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".7z"} | |
MEDIA_EXTS = {".mp3", ".wav", ".mp4", ".avi", ".mkv", ".mov"} | |
DOC_EXTS = {".pdf", ".doc", ".docx"} | |
ALLOWED_TARGET_EXTS: list[str] = sorted( | |
IMAGE_EXTS | TEXT_EXTS | ARCHIVE_EXTS | MEDIA_EXTS | DOC_EXTS | |
) | |
DISALLOWED_SOURCE_EXTS = {".exe", ".bin"} | |
# ----------------------------------------------------------------------------- | |
# Helpers ---------------------------------------------------------------------- | |
# ----------------------------------------------------------------------------- | |
def sidebar_target_extension() -> str: | |
st.sidebar.header("Settings") | |
query = st.sidebar.text_input("Filter extensions… (optional)") | |
filtered = [e for e in ALLOWED_TARGET_EXTS if query.lower() in e] | |
if not filtered: | |
st.sidebar.error("No extension matches that filter.") | |
target_ext = st.sidebar.selectbox( | |
"Choose target extension (applied to **all** files)", | |
filtered or ALLOWED_TARGET_EXTS, | |
index=(filtered or ALLOWED_TARGET_EXTS).index(".png") | |
if ".png" in (filtered or ALLOWED_TARGET_EXTS) else 0, | |
) | |
st.sidebar.markdown( | |
"*Images are truly converted. Text files are re‑saved as UTF‑8. " | |
"Other combinations fall back to a safe rename.*" | |
) | |
return target_ext | |
def uploader(): | |
return st.file_uploader( | |
"Upload any files (multiple allowed)", | |
accept_multiple_files=True, | |
type=None, # accept *all* extensions | |
help="Drag‑and‑drop or click to browse.", | |
) | |
# ----------------------------------------------------------------------------- | |
# Conversion logic ------------------------------------------------------------- | |
# ----------------------------------------------------------------------------- | |
def convert_image(data: bytes, target_ext: str) -> bytes: | |
"""Return `bytes` of the image converted to `target_ext`. Raises if Pillow | |
cannot save in that format.""" | |
img = Image.open(io.BytesIO(data)) | |
buf = io.BytesIO() | |
# Map certain extensions to Pillow format names | |
pil_fmt = { | |
".jpg": "JPEG", ".jpeg": "JPEG", ".png": "PNG", ".gif": "GIF", | |
".bmp": "BMP", ".tiff": "TIFF", ".ico": "ICO", ".webp": "WEBP", | |
}[target_ext] | |
img.save(buf, format=pil_fmt) | |
buf.seek(0) | |
return buf.read() | |
def convert_text(data: bytes, _target_ext: str) -> bytes: | |
"""Return data re‑encoded as UTF‑8 (no format change).""" | |
text = data.decode("utf‑8", errors="ignore") | |
return text.encode("utf‑8") | |
def convert_file(file: st.runtime.uploaded_file_manager.UploadedFile, target_ext: str) -> tuple[bytes, str]: | |
"""Try to convert and return (bytes, conversion_note). On failure, return | |
original data with a note that only rename happened.""" | |
orig_ext = Path(file.name).suffix.lower() | |
raw = file.read() | |
try: | |
if orig_ext in IMAGE_EXTS and target_ext in IMAGE_EXTS: | |
return convert_image(raw, target_ext), "image converted" | |
if orig_ext in TEXT_EXTS and target_ext in TEXT_EXTS: | |
return convert_text(raw, target_ext), "text re‑encoded" | |
except Exception as err: | |
st.warning(f"⚠️ Could not convert **{file.name}**: {err}. Falling back to rename.") | |
# Fallback: no conversion | |
return raw, "renamed only" | |
# ----------------------------------------------------------------------------- | |
# Zip packaging --------------------------------------------------------------- | |
# ----------------------------------------------------------------------------- | |
def package_zip(files: list[st.runtime.uploaded_file_manager.UploadedFile], target_ext: str) -> io.BytesIO: | |
buf = io.BytesIO() | |
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: | |
for file in files: | |
orig_path = Path(file.name) | |
if orig_path.suffix.lower() in DISALLOWED_SOURCE_EXTS: | |
st.warning(f"⏭️ Skipping disallowed file: **{orig_path.name}**") | |
continue | |
data, note = convert_file(file, target_ext) | |
new_name = orig_path.with_suffix(target_ext).name | |
zf.writestr(new_name, data) | |
st.success(f"✅ {note} • **{orig_path.name}** → **{new_name}**") | |
buf.seek(0) | |
return buf | |
# ----------------------------------------------------------------------------- | |
# Main ------------------------------------------------------------------------ | |
# ----------------------------------------------------------------------------- | |
def main(): | |
st.set_page_config("Universal Format Changer", page_icon="🔄", layout="centered") | |
st.title("🔄 Universal File‑Format Changer") | |
st.write("Upload files, pick a target extension, and download a ZIP with the converted files.") | |
target_ext = sidebar_target_extension() | |
files = uploader() | |
if files and st.button("🚀 Convert & Download"): | |
zip_buf = package_zip(files, target_ext) | |
ts = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ") | |
st.download_button( | |
"⬇️ Get ZIP", zip_buf, file_name=f"converted_{ts}.zip", mime="application/zip" | |
) | |
st.caption("© 2025 Universal Changer • Streamlit • Hugging Face Spaces") | |
if __name__ == "__main__": | |
main() | |