File Metadata Viewer — Python Tkinter App to Inspect Files (SHA256, EXIF, Export JSON) — FuzzuTech
Demo :
Click Video πππ
Description :
File Metadata Viewer — Inspect any file with a modern Tkinter GUI: SHA256, MIME, EXIF, preview & export JSON. Python + Pillow + pyperclip demo by FuzzuTech.
Features :
-
Modern dark Tkinter GUI for quick file inspection
-
Shows file name, full path, human-readable size
-
Displays created / modified / accessed timestamps
-
Detects MIME type & extension
-
Computes SHA256 hash (chunked reading)
-
EXIF extraction & image preview (Pillow)
-
Export metadata as pretty JSON
-
Copy values to clipboard (pyperclip)
-
Lightweight, single-file demo (
main.py) + sample assets
Code :
"""
FileMetadataViewer - Modern Tkinter GUI
Author: Fuzzu Developer (add your name)
Run: python main.py
Requires: pillow, pyperclip
"""
import os
import sys
import json
import mimetypes
import hashlib
import platform
from datetime import datetime
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter.scrolledtext import ScrolledText
from PIL import Image, ImageTk, ExifTags
import pyperclip
# ---------- Helpers ----------
def format_bytes(size):
# simple human readable
for unit in ['B','KB','MB','GB','TB']:
if size < 1024.0:
return f"{size:.2f} {unit}"
size /= 1024.0
return f"{size:.2f} PB"
def sha256sum(path, chunk_size=8192):
h = hashlib.sha256()
with open(path, 'rb') as f:
for block in iter(lambda: f.read(chunk_size), b''):
h.update(block)
return h.hexdigest()
def file_times(path):
s = os.stat(path)
# Note: on some systems ctime is change-time
return {
"created": datetime.fromtimestamp(getattr(s, 'st_ctime')).strftime("%Y-%m-%d %H:%M:%S"),
"modified": datetime.fromtimestamp(s.st_mtime).strftime("%Y-%m-%d %H:%M:%S"),
"accessed": datetime.fromtimestamp(s.st_atime).strftime("%Y-%m-%d %H:%M:%S"),
}
def get_mime(path):
mime, _ = mimetypes.guess_type(path)
return mime or "Unknown"
def get_exif(path):
try:
img = Image.open(path)
exif_raw = img._getexif()
if not exif_raw:
return {}
exif = {}
for tag_id, value in exif_raw.items():
tag = ExifTags.TAGS.get(tag_id, tag_id)
exif[tag] = value
return exif
except Exception:
return {}
# ---------- GUI ----------
class App(ttk.Frame):
def __init__(self, root):
super().__init__(root)
self.root = root
self.setup_root()
self.create_styles()
self.create_widgets()
self.pack(fill="both", expand=True)
self.current_image_tk = None
self.current_file = None
self.metadata = {}
def setup_root(self):
self.root.title("File Metadata Viewer — Fuzzu Developer")
try:
self.root.iconphoto(False, tk.PhotoImage(file=os.path.join("assets","icon.png")))
except Exception:
pass
self.root.geometry("820x520")
# make window responsive
self.root.minsize(780, 480)
def create_styles(self):
style = ttk.Style()
# Use default theme but tweak
style.theme_use("clam")
style.configure("TFrame", background="#0f1720")
style.configure("TLabel", background="#0f1720", foreground="white", font=("Inter", 11))
style.configure("Header.TLabel", font=("Inter", 16, "bold"), foreground="#FFD166")
style.configure("Accent.TButton", background="#FF6B6B", foreground="white", font=("Inter", 11, "bold"))
style.map("Accent.TButton",
background=[("active", "#FF4B4B")])
style.configure("Treeview", background="#0b1220", fieldbackground="#0b1220", foreground="white")
style.configure("TEntry", fieldbackground="#0b1220", foreground="white")
def create_widgets(self):
# top bar
top = ttk.Frame(self)
top.pack(fill="x", padx=12, pady=(12,6))
title = ttk.Label(top, text="File Metadata Viewer", style="Header.TLabel")
title.pack(side="left")
btn_frame = ttk.Frame(top)
btn_frame.pack(side="right")
select_btn = ttk.Button(btn_frame, text="Select File", style="Accent.TButton", command=self.select_file)
select_btn.pack(side="left", padx=6)
export_btn = ttk.Button(btn_frame, text="Export JSON", command=self.export_json)
export_btn.pack(side="left", padx=6)
copy_btn = ttk.Button(btn_frame, text="Copy Selected", command=self.copy_selected)
copy_btn.pack(side="left", padx=6)
# main area
main = ttk.Frame(self)
main.pack(fill="both", expand=True, padx=12, pady=8)
# left: metadata tree / details
left = ttk.Frame(main)
left.pack(side="left", fill="both", expand=True)
# info tree
self.tree = ttk.Treeview(left, columns=("property", "value"), show="headings", selectmode="browse", height=18)
self.tree.heading("property", text="Property")
self.tree.heading("value", text="Value")
self.tree.column("property", width=200, anchor="w")
self.tree.column("value", anchor="w")
self.tree.pack(fill="both", expand=True, pady=(0,8))
# small details row
details_frame = ttk.Frame(left)
details_frame.pack(fill="x", pady=(0,6))
self.label_path = ttk.Label(details_frame, text="Path: —", wraplength=380)
self.label_path.pack(anchor="w")
self.label_size = ttk.Label(details_frame, text="Size: —")
self.label_size.pack(side="left", padx=(0,8))
self.label_mime = ttk.Label(details_frame, text="MIME: —")
self.label_mime.pack(side="left")
# right: preview + raw metadata
right = ttk.Frame(main, width=320)
right.pack(side="right", fill="y", padx=(8,0))
preview_lbl = ttk.Label(right, text="Preview", font=("Inter", 12, "bold"))
preview_lbl.pack(anchor="center", pady=(6,4))
self.canvas = tk.Label(right, background="#0f1720")
self.canvas.pack(ipadx=10, ipady=10, pady=(0,8))
exif_lbl = ttk.Label(right, text="Raw / EXIF", font=("Inter", 12, "bold"))
exif_lbl.pack(anchor="w", padx=6)
self.raw = ScrolledText(right, height=10, wrap="none", background="#071018", foreground="white")
self.raw.pack(fill="both", expand=True, padx=6, pady=(4,0))
# status bar
self.status = ttk.Label(self, text="Ready", anchor="w")
self.status.pack(fill="x", side="bottom", padx=12, pady=(6,12))
# bindings
self.tree.bind("<Double-1>", self.on_tree_double)
self.tree.bind("<<TreeviewSelect>>", lambda e: self.on_select())
# ---------- Actions ----------
def set_status(self, txt):
self.status.config(text=txt)
def select_file(self):
path = filedialog.askopenfilename()
if not path:
return
self.load_file(path)
def load_file(self, path):
self.current_file = path
self.set_status("Loading metadata...")
self.tree.delete(*self.tree.get_children())
try:
meta = {}
meta["File Name"] = os.path.basename(path)
meta["File Path"] = os.path.abspath(path)
size = os.path.getsize(path)
meta["Size"] = format_bytes(size)
meta.update(file_times(path))
meta["MIME Type"] = get_mime(path)
meta["Extension"] = os.path.splitext(path)[1] or "—"
meta["SHA256"] = sha256sum(path)
meta["Platform"] = platform.system()
meta["Python Version"] = platform.python_version()
# put into tree
for k, v in meta.items():
self.tree.insert("", "end", values=(k, v))
# update small labels
self.label_path.config(text=f"Path: {meta['File Path']}")
self.label_size.config(text=f"Size: {meta['Size']}")
self.label_mime.config(text=f"MIME: {meta['MIME Type']}")
# EXIF for images
exif = {}
if meta["MIME Type"] and meta["MIME Type"].startswith("image"):
exif = get_exif(path)
# show preview
self.show_image_preview(path)
else:
# clear preview
self.canvas.config(image="", text="No preview", compound="center", width=280, height=180, fg="white")
self.current_image_tk = None
# raw text
raw_data = {
"metadata": meta,
"exif": exif
}
pretty = json.dumps(raw_data, indent=2, default=str)
self.raw.delete("1.0", tk.END)
self.raw.insert(tk.END, pretty)
# store
self.metadata = raw_data
self.set_status("Metadata loaded")
except Exception as e:
messagebox.showerror("Error", f"Failed to load file: {e}")
self.set_status("Error loading file")
def show_image_preview(self, path):
try:
img = Image.open(path)
# maintain aspect but fit into box
max_w, max_h = 280, 180
img.thumbnail((max_w, max_h), Image.ANTIALIAS)
self.current_image_tk = ImageTk.PhotoImage(img)
self.canvas.config(image=self.current_image_tk, text="")
except Exception as e:
self.canvas.config(text="Preview error", image="")
self.current_image_tk = None
def export_json(self):
if not self.current_file or not self.metadata:
messagebox.showinfo("No file", "Please load a file first.")
return
path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON file","*.json")])
if not path:
return
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(self.metadata, f, indent=2, default=str)
self.set_status(f"Exported JSON → {path}")
messagebox.showinfo("Exported", f"Metadata exported to:\n{path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to export: {e}")
def copy_selected(self):
sel = self.tree.selection()
if not sel:
messagebox.showinfo("Select", "Select a property row to copy its value.")
return
val = self.tree.item(sel[0], 'values')[1]
pyperclip.copy(str(val))
self.set_status("Copied to clipboard")
def on_tree_double(self, event):
sel = self.tree.selection()
if not sel:
return
prop, val = self.tree.item(sel[0], 'values')
# show a small dialog with full value and copy button
dlg = tk.Toplevel(self.root)
dlg.title(prop)
dlg.geometry("500x220")
dlg.configure(bg="#0b1220")
lbl = ttk.Label(dlg, text=prop, font=("Inter", 12, "bold"))
lbl.pack(pady=(10,6))
txt = ScrolledText(dlg, height=6, wrap="word", background="#071018", foreground="white")
txt.insert("1.0", str(val))
txt.pack(fill="both", expand=True, padx=10, pady=(0,6))
def do_copy():
pyperclip.copy(str(val))
messagebox.showinfo("Copied", "Value copied to clipboard.")
btn = ttk.Button(dlg, text="Copy", command=do_copy)
btn.pack(pady=(0,10))
dlg.transient(self.root)
dlg.grab_set()
dlg.wait_window()
def on_select(self):
# update status with selected property
sel = self.tree.selection()
if sel:
prop, val = self.tree.item(sel[0], 'values')
self.set_status(f"Selected: {prop}")
# ---------- Run ----------
def main():
root = tk.Tk()
# prefer dark background for the whole root
root.configure(bg="#0f1720")
app = App(root)
root.mainloop()
if __name__ == "__main__":
main()
Comments
Post a Comment