0% found this document useful (0 votes)
7 views7 pages

Usrbinenv Python3

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
7 views7 pages

Usrbinenv Python3

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 7

#!

/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import io
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk, ImageFilter, ImageOps, ImageEnhance

APP = "Laboratorio de Mapas de Bits (Tkinter + Pillow)"

class BitmapLab(tk.Tk):
def __init__(self):
super().__init__()
self.title(APP); self.geometry("1000x680"); self.minsize(900,
600)

# Estado
self.img_orig = None # Image base (cargada)
self.img_work = None # Image tras pipeline
self.img_disp = None # Image para canvas (ajustada)
self.view_fit = True # Ajustar a ventana
self.crop_mode = False
self.crop_start = None
self.crop_rect = None

self._build_ui()

# --- UI ---
def _build_ui(self):
# Top bar
top = ttk.Frame(self); top.pack(fill="x", padx=10, pady=6)
ttk.Button(top, text="Abrir...",
command=self.open_image).pack(side="left")
ttk.Button(top, text="Guardar como...",
command=self.save_image).pack(side="left", padx=6)
ttk.Button(top, text="Reset",
command=self.reset_image).pack(side="left", padx=(6,0))
ttk.Separator(top, orient="vertical").pack(side="left", fill="y",
padx=8)
self.lbl_info = ttk.Label(top, text="Sin imagen");
self.lbl_info.pack(side="left")
self.fit_var = tk.BooleanVar(value=True)
ttk.Checkbutton(top, text="Ajustar a ventana",
variable=self.fit_var,
command=self.render).pack(side="right")

# Main split
main = ttk.Frame(self); main.pack(fill="both", expand=True,
padx=10, pady=(0,10))
self.canvas = tk.Canvas(main, bg="#111318", highlightthickness=0)
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.bind("<Configure>", lambda e: self.render())
self.canvas.bind("<Button-1>", self.on_canvas_down)
self.canvas.bind("<B1-Motion>", self.on_canvas_drag)
self.canvas.bind("<ButtonRelease-1>", self.on_canvas_up)

# Panel de controles
ctrl = ttk.Notebook(main); ctrl.pack(side="left", fill="y",
padx=(10,0))
ctrl.enable_traversal()

# --- Pestaña Filtros rápidos


tab1 = ttk.Frame(ctrl); ctrl.add(tab1, text="Filtros")

self.var_gray = tk.BooleanVar(); self.var_invert =


tk.BooleanVar()

ttk.Checkbutton(tab1, text="Escala de grises",


variable=self.var_gray, command=self.apply).pack(anchor="w", padx=10,
pady=4)
ttk.Checkbutton(tab1, text="Invertir colores",
variable=self.var_invert, command=self.apply).pack(anchor="w", padx=10,
pady=4)

ttk.Label(tab1, text="Bordes / Enfoque /


Desenfoque").pack(anchor="w", padx=10, pady=(8,2))
ttk.Button(tab1, text="Detectar bordes (Sobel)",
command=lambda:self.apply(filter="edges")).pack(fill="x", padx=10,
pady=2)
ttk.Button(tab1, text="Enfocar (Sharpen)",
command=lambda:self.apply(filter="sharpen")).pack(fill="x", padx=10,
pady=2)
ttk.Button(tab1, text="Desenfoque Gaussiano",
command=lambda:self.apply(filter="gauss")).pack(fill="x", padx=10,
pady=2)

ttk.Label(tab1, text="Rotar / Voltear").pack(anchor="w", padx=10,


pady=(10,2))

btns = ttk.Frame(tab1); btns.pack(fill="x", padx=10)


ttk.Button(btns, text="○ 90°", command=lambda:self.rotate(-
90)).pack(side="left", expand=True, fill="x", padx=2)
ttk.Button(btns, text="○' 90°",
command=lambda:self.rotate(90)).pack(side="left", expand=True, fill="x",
padx=2)
ttk.Button(btns, text="180°",
command=lambda:self.rotate(180)).pack(side="left", expand=True, fill="x",
padx=2)

btns2 = ttk.Frame(tab1); btns2.pack(fill="x", padx=10,


pady=(4,0))
ttk.Button(btns2, text="Voltear H",
command=lambda:self.flip("h")).pack(side="left", expand=True, fill="x",
padx=2)
ttk.Button(btns2, text="Voltear V",
command=lambda:self.flip("v")).pack(side="left", expand=True, fill="x",
padx=2)
ttk.Separator(tab1).pack(fill="x", padx=10, pady=10)
ttk.Button(tab1, text="Modo recorte (dibujar rectángulo)",
command=self.toggle_crop).pack(fill="x", padx=10, pady=(0,8))

# --- Pestaña Ajustes finos


tab2 = ttk.Frame(ctrl); ctrl.add(tab2, text="Ajustes")

self.s_bri = self._slider(tab2, "Brillo", 0.0, 3.0, 1.0)


self.s_con = self._slider(tab2, "Contraste", 0.0, 3.0, 1.0)
self.s_sat = self._slider(tab2, "Color (saturación)", 0.0, 3.0,
1.0)
self.s_shp = self._slider(tab2, "Nitidez", 0.0, 3.0, 1.0)
self.s_blr = self._slider(tab2, "Blur gaussiano (px)", 0.0, 10.0,
0.0, step=0.1)

ttk.Button(tab2, text="Restablecer ajustes",


command=self.reset_sliders).pack(fill="x", padx=10, pady=8)

# --- Pestaña Info


tab3 = ttk.Frame(ctrl); ctrl.add(tab3, text="Info")
self.txt_info = tk.Text(tab3, height=12, width=36, bg="#0f1218",
fg="#e6eaf2", bd=0)
self.txt_info.pack(fill="both", expand=True, padx=8, pady=8)
self._log("Abre una imagen (PNG/JPG/BMP/WebP...).")

def _slider(self, parent, label, mn, mx, val, step=0.05):


frm = ttk.Frame(parent); frm.pack(fill="x", padx=10, pady=6)
ttk.Label(frm, text=label).pack(anchor="w")
var = tk.DoubleVar(value=val)
s = ttk.Scale(frm, from_=mn, to=mx, variable=var,
orient="horizontal", command=lambda *_:self.apply())
s.pack(fill="x")
box = ttk.Frame(frm); box.pack(fill="x")
ttk.Label(box, text=f"{mn:.1f}").pack(side="left")
lbl = ttk.Label(box, text=f"{val:.2f}"); lbl.pack(side="left",
expand=True)
ttk.Label(box, text=f"{mx:.1f}").pack(side="right")
var.trace_add("write", lambda *_:
lbl.config(text=f"{var.get():.2f}"))
return var

# --- Carga/Guardado ---


def open_image(self):
path =
filedialog.askopenfilename(filetypes=[("Imágenes","*.png;*.jpg;*.jpeg;*.b
mp;*.webp;*.tiff"),("Todos","*.*")])
if not path: return
try:
im = Image.open(path).convert("RGB")
except Exception as e:
messagebox.showerror("Error", f"No se pudo abrir:\n{e}");
return
self.img_orig = im
self.reset_sliders()
self.var_gray.set(False); self.var_invert.set(False)
self.crop_mode = False; self.crop_start = None
self._log(f"Archivo: {os.path.basename(path)} |
{im.width}x{im.height} px, RGB")
self.lbl_info.config(text=f"{im.width}x{im.height}px -
{os.path.basename(path)}")
self.apply()

def save_image(self):
if self.img_work is None:
messagebox.showinfo("Sin imagen", "Nada que guardar.");
return
path = filedialog.asksaveasfilename(defaultextension=".png",
filetypes=[("PNG","*.png"),("JPEG","*.jpg"),("WebP","*.webp"),("BMP","*.b
mp")])
if not path: return
try:
self.img_work.save(path)
self._log(f"Guardado: {os.path.basename(path)}")
except Exception as e:
messagebox.showerror("Error", f"No se pudo guardar:\n{e}")

def reset_image(self):
if self.img_orig is None: return
self.reset_sliders()
self.var_gray.set(False); self.var_invert.set(False)
self.crop_mode = False; self.crop_start = None
self.apply()

def reset_sliders(self):
self.s_bri.set(1.0); self.s_con.set(1.0); self.s_sat.set(1.0);
self.s_shp.set(1.0); self.s_blr.set(0.0)

# --- Procesamiento ---


def apply(self, filter=None):
if self.img_orig is None: return
im = self.img_orig.copy()

# Filtros "rápidos"
if self.var_gray.get():
im = ImageOps.grayscale(im).convert("RGB")

if self.var_invert.get():
# si está en L, invert produce L; normaliza a RGB
im = ImageOps.invert(im.convert("RGB"))

if filter == "edges":
im = im.filter(ImageFilter.FIND_EDGES)
elif filter == "sharpen":
im = im.filter(ImageFilter.UnsharpMask(radius=1.8,
percent=160, threshold=3))
elif filter == "gauss":
im = im.filter(ImageFilter.GaussianBlur(radius=2.0))
# Ajustes finos
im = ImageEnhance.Brightness(im).enhance(self.s_bri.get())
im = ImageEnhance.Contrast(im).enhance(self.s_con.get())
im = ImageEnhance.Color(im).enhance(self.s_sat.get())
im = ImageEnhance.Sharpness(im).enhance(self.s_shp.get())
blur = self.s_blr.get()
if blur > 0.01:
im = im.filter(ImageFilter.GaussianBlur(radius=float(blur)))
self.img_work = im
self.render()

def rotate(self, deg):


if self.img_orig is None: return
self.img_orig = self.img_orig.rotate(-deg, expand=True) # PIL
rota antihoraria
self.apply()

def flip(self, axis):


if self.img_orig is None: return
if axis == "h":
self.img_orig = ImageOps.mirror(self.img_orig)
else:
self.img_orig = ImageOps.flip(self.img_orig)
self.apply()

# --- Recorte ---


def toggle_crop(self):
if self.img_orig is None: return
self.crop_mode = not self.crop_mode
self._log("Modo recorte: " + ("ON (arrastre para seleccionar)" if
self.crop_mode else "OFF"))

def on_canvas_down(self, e):


if not self.crop_mode or self.img_work is None: return
self.crop_start = (e.x, e.y)
if self.crop_rect: self.canvas.delete(self.crop_rect)
self.crop_rect = None

def on_canvas_drag(self, e):


if not self.crop_mode or self.crop_start is None: return
if self.crop_rect: self.canvas.delete(self.crop_rect)
self.crop_rect = self.canvas.create_rectangle(self.crop_start[0],
self.crop_start[1], e.x, e.y, outline="#ffd166", width=2, dash=(4,2))

def on_canvas_up(self, e):


if not self.crop_mode or self.crop_start is None: return
x0, y0 = self.crop_start; x1, y1 = e.x, e.y
self.crop_start = None
if abs(x1-x0) < 4 or abs(y1-y0) < 4:
return
# Convertir coords de canvas a coords de imagen
box = self._canvas_to_image_box(min(x0,x1), min(y0,y1),
max(x0,x1), max(y0,y1))
if box is None: return
L, U, R, D = box
try:
self.img_orig = self.img_work.crop([L, U, R, D])
self._log(f"Recorte: {R-L}×{D-U}px")
self.crop_mode = False
if self.crop_rect: self.canvas.delete(self.crop_rect);
self.crop_rect=None
self.apply()
except Exception as ex:
messagebox.showerror("Recorte", f"No se pudo recortar: {ex}")

def _canvas_to_image_box(self, x0, y0, x1, y1):


"""Mapea un rectángulo del canvas a la caja en la imagen work."""
if self.img_work is None: return None
cw, ch = self.canvas.winfo_width(), self.canvas.winfo_height()
iw, ih = self.img_work.width, self.img_work.height

# calcular destino de la imagen en el canvas (para mantener


relación de aspecto)
scale = min(cw/iw, ch/ih) if self.fit_var.get() else 1.0
dw, dh = int(iw*scale), int(ih*scale)
ox = (cw - dw)//2
oy = (ch - dh)//2

# limitar a área dibujada


x0 = max(ox, min(x0, ox+dw)); x1 = max(ox, min(x1, ox+dw))
y0 = max(oy, min(y0, oy+dh)); y1 = max(oy, min(y1, oy+dh))
if x1<=x0 or y1<=y0: return None

# convertir a coords de imagen


L = int((x0-ox)/scale); U = int((y0-oy)/scale); R = int((x1-
ox)/scale); D = int((y1-oy)/scale)
return (L, U, R, D)

# --- Render ---


def render(self):
self.canvas.delete("all")
if self.img_work is None: return
im = self.img_work
cw, ch = self.canvas.winfo_width(), self.canvas.winfo_height()

if self.fit_var.get():
scale = min(cw/im.width, ch/im.height)
scale = max(scale, 0.05)
show = im.resize((int(im.width*scale), int(im.height*scale)),
Image.LANCZOS)
else:
show = im.copy()

self.img_disp = ImageTk.PhotoImage(show)
x = (cw - show.width)/2
y = (ch - show.height)/2
self.canvas.create_image(x, y, anchor="nw", image=self.img_disp)
# --- Util ---
def _log(self, msg):
self.txt_info.insert("end", msg + "\n")
self.txt_info.see("end")

if __name__ == "__main__":
BitmapLab().mainloop()

You might also like