euler314's picture
Update app.py
428bf45 verified
raw
history blame
6.73 kB
"""
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()