Este tutorial adapta pedagógicamente el Día 85. La sección 📝 Contenido original del curso proviene del único archivo de la carpeta:
dia85-tarea4.md(enunciado y pregunta de reflexión, en inglés; aquí traducido). El curso no ofrece guía ni solución, y este tutorial no entrega la aplicación terminada: ofrece el análisis, las piezas de Pillow verificadas, un esqueleto de interfaz para completar, pruebas y retos. Todo lo que no está en ese archivo (conceptos de imágenes, capas y transparencia, código de ejemplo, sección de derechos de autor y privacidad, retos 🟢🟡🟠🔴, proyecto extra y la etapa 4 de Mi Portafolio Profesional) es creación de esta adaptación.
Nota sobre las verificaciones: el 28 de septiembre de 2026, con Python 3.14 y Pillow 12.3.0, se ejecutaron las funciones
agregar_marca_textoyagregar_logode la sección 4 sobre imágenes generadas para la prueba (800 × 600). También se comprobó queImage.ANTIALIASyImageDraw.textsizeya no existen en Pillow y que guardar una imagen RGBA como JPEG produceOSError: cannot write mode RGBA as JPEG. La interfaz Tkinter de la sección 5 es un esqueleto para completar y no se probó con clics reales.
Tabla de contenidos
- La tarea: marcas de agua automáticas
- Analizar el problema antes de programar
- 📖 Cómo "piensa" Pillow una imagen: modos, capas y transparencia
- 📖 La lógica: agregar texto y logo (sin interfaz)
- 📖 La interfaz: abrir, previsualizar y guardar con Tkinter
- 📖 Derechos de autor y privacidad de las imágenes
- 🚀 Proyecto del día: construcción por versiones
- 🌟 Proyecto extra: generador de carnés para un evento
- 🏗️ Mi Portafolio Profesional — Etapa 4
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Observaciones sobre el material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 85
1. La tarea: marcas de agua automáticas
(Fuente: dia85-tarea4.md)
📝 Contenido original del curso
Usando lo que aprendiste de Tkinter, crea una aplicación de escritorio con interfaz gráfica (GUI) en la que puedas cargar una imagen y usar Python para agregarle una marca de agua (un logo o un texto).
Normalmente tendrías que usar un programa de edición como Photoshop; tu programa lo hará automáticamente.
Caso de uso: quieres empezar a publicar tus fotos en Instagram, pero quieres que todas lleven tu sitio web. Con tu software puedes agregar tu sitio o logo automáticamente a cualquier imagen. Un servicio en línea parecido es watermarkly.com.
Quizá necesites: la librería Pillow (pypi.org/project/Pillow), la documentación de tkinter (docs.python.org) "y algo de Google".
Pregunta de la tarea — Momento de reflexión: cómo abordaste el proyecto; qué fue difícil y qué fácil; cómo mejorarías; tu mayor aprendizaje; qué harías distinto.
Prerequisitos: Tkinter: ventanas, botones, etiquetas, entradas y
grid(Días 27–29); funciones y parámetros (Días 8 y 10); manejo de archivos y rutas (Día 24); excepciones (Día 30); instalar paquetes conpip(Día 15).
2. Analizar el problema antes de programar
(Sección creada para esta adaptación.)
🔍 Antes de programar
| Pregunta | Respuesta que debes construir tú |
|---|---|
| ¿Qué información tenemos? | Una imagen del usuario y un texto o logo |
| ¿Qué necesitamos obtener del usuario? | Qué imagen, qué texto (o qué logo), dónde ponerlo, qué tan transparente |
| ¿Qué proceso se realiza? | Abrir imagen → crear la marca → combinarlas → mostrar el resultado → guardarlo |
| ¿Qué resultado esperamos? | Un archivo nuevo con la marca; el original intacto |
| ¿Qué condiciones hay que tener en cuenta? | Imágenes grandes o pequeñas, fotos verticales, archivos que no son imágenes, PNG con transparencia |
Dos capas separadas: lógica e interfaz
Un error típico es mezclar todo dentro de la función de un botón. Separa:
marcas.py (lógica, sin Tkinter) app.py (interfaz)
┌───────────────────────────────┐ ┌──────────────────────────┐
│ agregar_marca_texto(...) │ ◄───── │ botón "Aplicar" │
│ agregar_logo(...) │ │ botones "Abrir"/"Guardar"│
│ se prueba sin abrir ventanas │ │ vista previa │
└───────────────────────────────┘ └──────────────────────────┘Ventaja: puedes probar marcas.py con assert y, en el futuro, reutilizarlo en una página web (Día 92) o en un script que procese una carpeta entera.
3. 📖 Cómo "piensa" Pillow una imagen: modos, capas y transparencia
(Sección creada para esta adaptación.)
🎯 Objetivo de aprendizaje
Explicar qué es el canal alfa y por qué la marca de agua se dibuja en una capa aparte.
📖 Explicación conceptual
Una imagen digital es una cuadrícula de píxeles. Cada píxel guarda números según el modo:
| Modo | Cada píxel guarda | Ejemplo | Formatos típicos |
|---|---|---|---|
RGB |
Rojo, verde, azul (0–255) | (30, 90, 160) |
JPEG, PNG |
RGBA |
RGB + alfa (opacidad: 0 transparente, 255 opaco) | (255, 255, 255, 128) = blanco semitransparente |
PNG (el JPEG no admite alfa) |
L |
Un solo valor de gris | 128 |
Ambos |
Una marca de agua "semitransparente" es un texto con alfa intermedio (por ejemplo, 128). Para que la transparencia se mezcle bien con la foto:
- Convertir la foto a
RGBA. - Crear una capa del mismo tamaño, totalmente transparente.
- Dibujar el texto en esa capa.
- Componer ambas capas con
Image.alpha_composite.
Es la misma idea de las capas de un editor de fotos.
Coordenadas: en Pillow (como en Tkinter y en turtle con otro origen), el punto (0, 0) es la esquina superior izquierda; x crece hacia la derecha y y hacia abajo.
4. 📖 La lógica: agregar texto y logo (sin interfaz)
(Sección creada para esta adaptación. Código verificado con Pillow 12.3.0.)
Instala Pillow en tu entorno virtual: pip install Pillow (el paquete se llama Pillow, pero se importa como PIL).
🔎 Ejemplo explicado: texto en la esquina inferior derecha
from PIL import Image, ImageDraw, ImageFont
def agregar_marca_texto(ruta_entrada, ruta_salida, texto, opacidad=128):
with Image.open(ruta_entrada) as original:
imagen = original.convert("RGBA")
capa = Image.new("RGBA", imagen.size, (255, 255, 255, 0))
dibujo = ImageDraw.Draw(capa)
tamano = max(12, imagen.width // 20)
try:
fuente = ImageFont.truetype("arial.ttf", tamano)
except OSError:
fuente = ImageFont.load_default(size=tamano)
izquierda, arriba, derecha, abajo = dibujo.textbbox((0, 0), texto, font=fuente)
ancho, alto = derecha - izquierda, abajo - arriba
margen = tamano // 2
posicion = (imagen.width - ancho - margen, imagen.height - alto - margen)
dibujo.text(posicion, texto, font=fuente, fill=(255, 255, 255, opacidad))
resultado = Image.alpha_composite(imagen, capa)
resultado.convert("RGB").save(ruta_salida, "JPEG", quality=95)Qué hace cada parte:
with Image.open(...)abre el archivo y lo cierra al terminar (Día 24).Image.new("RGBA", imagen.size, (255, 255, 255, 0)): capa del mismo tamaño, blanca pero con alfa 0 (invisible).tamano = imagen.width // 20: el texto se adapta a la imagen. Con un tamaño fijo, en una foto de 4000 px la marca sería diminuta y en una de 300 px, gigante.ImageFont.truetype("arial.ttf", ...)busca la fuente Arial (existe en Windows). Si no la encuentra (Linux, macOS),load_default(size=...)usa la fuente incluida en Pillow. Eltry/exceptevita que el programa se cierre (Día 30).textbbox((0, 0), texto, font=fuente)devuelve la "caja" que ocupa el texto: con ella se calcula dónde ubicarlo para que no se salga.alpha_compositemezcla la foto y la capa respetando la transparencia..convert("RGB")antes de guardar como JPEG: el JPEG no acepta alfa.
Tu turno: el logo
La idea para un logo PNG con transparencia es parecida (verificada): abrir el logo en RGBA, redimensionarlo proporcionalmente (por ejemplo, al 20 % del ancho de la foto) con logo.resize((ancho, alto), Image.Resampling.LANCZOS) y pegarlo con imagen.paste(logo, posicion, logo). El tercer argumento (la máscara) hace que las partes transparentes del logo no tapen la foto.
Pregunta: si el logo mide 200 × 100 y quieres que tenga 160 px de ancho, ¿qué alto debe tener para no deformarse?
⚠️ Errores frecuentes
| Error | Qué ocurre | Por qué | Solución |
|---|---|---|---|
OSError: cannot write mode RGBA as JPEG |
No se guarda la imagen | El JPEG no tiene canal alfa | .convert("RGB") antes de guardar, o guardar como PNG |
AttributeError: ... has no attribute 'ANTIALIAS' |
Código de un tutorial antiguo falla | Pillow 10 (2023) eliminó Image.ANTIALIAS |
Usa Image.Resampling.LANCZOS |
AttributeError: ... 'textsize' |
Igual que el anterior | Pillow 10 eliminó ImageDraw.textsize |
Usa textbbox o textlength |
OSError: cannot open resource |
No encuentra la fuente | La ruta de la fuente no existe en ese sistema | try/except con load_default o incluye un .ttf con licencia libre en tu proyecto |
| El logo aparece con un fondo negro | La transparencia se pierde | Se pegó sin máscara | imagen.paste(logo, pos, logo) |
| Se sobrescribe la foto original | El usuario pierde su foto | Se guardó con la misma ruta | Guarda con otro nombre (foto_marca.jpg) o pregunta con "Guardar como" |
| La marca "desaparece" en fotos claras | Texto blanco sobre cielo blanco | Poco contraste | Permite elegir color, o agrega una sombra (dibuja el texto dos veces, desplazado) |
5. 📖 La interfaz: abrir, previsualizar y guardar con Tkinter
(Sección creada para esta adaptación. Esqueleto para completar; no se probó con clics reales.)
Tkinter trae los diálogos de archivos listos en tkinter.filedialog, y Pillow trae ImageTk para mostrar imágenes en la ventana.
import tkinter as tk
from tkinter import filedialog, messagebox
from PIL import Image, ImageTk
ventana = tk.Tk()
ventana.title("Marca de agua")
ruta_actual = None
vista = tk.Label(ventana, text="Abre una imagen")
vista.grid(row=0, column=0, columnspan=3)
def abrir():
global ruta_actual
ruta = filedialog.askopenfilename(
filetypes=[("Imágenes", "*.jpg *.jpeg *.png")])
if not ruta: # el usuario canceló
return
ruta_actual = ruta
with Image.open(ruta) as imagen:
miniatura = imagen.copy()
miniatura.thumbnail((500, 500)) # no deforma la imagen
foto = ImageTk.PhotoImage(miniatura)
vista.config(image=foto, text="")
vista.image = foto # ¡guardar una referencia!
tk.Button(ventana, text="Abrir imagen", command=abrir).grid(row=1, column=0)
# Tu turno: campo de texto, botón "Aplicar", botón "Guardar como"...
ventana.mainloop()Puntos clave:
askopenfilenamedevuelve la ruta elegida o una cadena vacía si el usuario cancela: compruébalo.thumbnail((500, 500))reduce la imagen manteniendo la proporción, solo para mostrarla. La marca debe aplicarse a la imagen original, en su tamaño completo.vista.image = foto: si no guardas una referencia aPhotoImage, Python la borra de la memoria y la imagen no se ve (la etiqueta queda en blanco sin mostrar error). Es uno de los errores más desconcertantes de Tkinter.- Para "Guardar como":
filedialog.asksaveasfilename(defaultextension=".jpg"). - Para avisar errores:
messagebox.showerror("Error", "No se pudo abrir el archivo")dentro de unexcept.
Alternativa de diseño: en lugar de global ruta_actual, crear una clase AppMarcaDeAgua con atributos (Días 16–17). Con más de dos o tres variables compartidas, la clase es más ordenada. Compara ambas y elige.
6. 📖 Derechos de autor y privacidad de las imágenes
(Sección creada para esta adaptación.)
Una marca de agua sirve para identificar tu obra. Úsala de forma responsable:
| ✅ Adecuado | ❌ No adecuado |
|---|---|
| Marcar tus fotos o las del club de fotografía, con permiso | Poner tu marca sobre fotos de otras personas para hacerlas pasar por tuyas |
| Marcar imágenes con licencia que lo permita (revisa la licencia) | Quitar marcas de agua ajenas: vulnera los derechos de autor |
| Probar con imágenes generadas por ti o paisajes | Probar con fotos de compañeros sin su consentimiento |
Privacidad: las fotos pueden llevar metadatos EXIF (fecha, modelo del celular e incluso coordenadas GPS). Al crear una imagen nueva con Pillow y guardarla sin pasar el EXIF, esos datos normalmente no se copian; compruébalo con las propiedades del archivo. Es una buena "característica de privacidad" para mencionar en tu README.md.
7. 🚀 Proyecto del día: construcción por versiones
(Desarrolla la tarea dia85-tarea4.md. Las versiones son una propuesta de esta adaptación.)
Problema
Construir una aplicación de escritorio que agregue una marca de agua (texto y/o logo) a una imagen.
Contexto
El club de fotografía del colegio publica las fotos de los eventos en su cuenta y quiere que todas lleven su nombre, sin tener que editarlas una por una.
Objetivo
Una aplicación Tkinter que abra una imagen, muestre una vista previa, aplique una marca configurable y guarde una copia.
Requisitos funcionales
- Abrir una imagen JPG o PNG con un diálogo de archivos.
- Escribir el texto de la marca y aplicarlo; ver el resultado en la vista previa.
- Guardar el resultado como archivo nuevo.
- Mostrar mensajes claros si algo falla (archivo no válido, nada que guardar).
Restricciones técnicas
- Lógica de imágenes en un módulo separado de la interfaz (sección 2).
- La marca debe escalar con el tamaño de la imagen.
- Nunca sobrescribir la imagen original sin preguntar.
Planificación
- Comprender el caso de uso (prueba watermarkly.com o un editor). 2. Entradas: imagen, texto/logo, opciones. 3. Procesos: abrir → componer → mostrar → guardar. 4. Salidas: la vista previa y el archivo. 5. Dividir en lógica e interfaz. 6. Bocetar la ventana. 7. Implementar por versiones. 8. Probar. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1 — Lógica en consola:
marcas.pyconagregar_marca_texto; pruébala con una imagen fija, sin ventanas. - Versión 2 — Ventana y vista previa: abrir una imagen y mostrar su miniatura.
- Versión 3 — Aplicar texto: campo de texto y botón "Aplicar" que actualiza la vista previa.
- Versión 4 — Guardar: "Guardar como" con el formato adecuado.
- Versión 5 — Opciones: posición (menú con esquinas y centro), opacidad (
tk.Scale), color. - Versión final — Logo y pulido: agregar logo PNG, mensajes de error,
README.mdcon capturas antes/después, diario.
Pruebas
| # | Caso | Resultado esperado |
|---|---|---|
| 1 | Foto horizontal grande (por ejemplo, 4000 × 3000) | Marca visible y proporcionada |
| 2 | Foto vertical pequeña (300 × 500) | La marca no se sale de la imagen |
| 3 | PNG con fondo transparente | Se guarda sin error (convertir o guardar como PNG) |
| 4 | Abrir un archivo .txt renombrado como .jpg |
Mensaje de error, la app no se cierra |
| 5 | Cancelar el diálogo de abrir | No pasa nada, no hay error |
| 6 | Texto vacío | La app avisa o no aplica nada |
| 7 | Guardar y abrir el resultado con otro programa | Se abre bien; el original no cambió |
Prueba automática de la lógica (sin ventanas):
from PIL import Image
from marcas import agregar_marca_texto
Image.new("RGB", (800, 600), (30, 90, 160)).save("prueba.jpg")
agregar_marca_texto("prueba.jpg", "prueba_marca.jpg", "@club_foto")
with Image.open("prueba_marca.jpg") as resultado:
assert resultado.size == (800, 600)
assert resultado.mode == "RGB"🧪 Prueba y depuración
def aplicar():
imagen = Image.open(ruta_actual)
dibujo = ImageDraw.Draw(imagen)
dibujo.text((10, 10), entrada.get(), fill=(255, 255, 255, 100))
imagen = imagen.resize((500, 500), Image.ANTIALIAS)
foto = ImageTk.PhotoImage(imagen)
vista.config(image=foto)
imagen.save(ruta_actual)Encuentra cinco problemas: el alfa no tiene efecto al dibujar directo sobre una imagen RGB (hace falta la capa y alpha_composite); ANTIALIAS ya no existe; resize((500, 500)) deforma la imagen (mejor thumbnail sobre una copia); no se guarda la referencia a foto; y se sobrescribe el original (¡con la versión reducida!).
⭐ Extensión del proyecto
- 🟢 Reto 1 — Aplicación: agrega una sombra al texto para que se lea sobre fondos claros y oscuros.
- 🟡 Reto 2 — Ampliación: marca de agua en mosaico (el texto repetido en diagonal por toda la imagen); pista: rota la capa con
capa.rotate(...). - 🟠 Reto 3 — Desafío: procesamiento por lotes: elegir una carpeta (
askdirectory) y marcar todas sus imágenes, guardándolas en una subcarpetacon_marca/(Día 24:pathlib). Muestra el progreso. - 🔴 Reto 4 — Pensamiento computacional: diseña una forma de elegir automáticamente el color de la marca (blanco o negro) según el brillo de la zona donde irá. ¿Cómo mides el brillo de una región? (Pista:
imagen.crop(...)y el promedio de sus píxeles en modoL.) Prueba tu regla con 5 fotos distintas.
8. 🌟 Proyecto extra: generador de carnés para un evento
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
Para la feria de ciencia del colegio se necesitan carnés para los expositores: una plantilla con el logo de la feria, el nombre del expositor, su proyecto y su mesa.
Requisitos
- Leer los datos desde un CSV (nombre, proyecto, mesa) con
csvo pandas (Día 25). - Generar una imagen PNG por persona sobre una plantilla, centrando el texto (pista:
textbbox). - Una interfaz Tkinter para elegir la plantilla y el CSV y ver la vista previa del primer carné.
Decisiones que debes tomar (y justificar)
- ¿Qué pasa con un nombre muy largo? ¿Reduces la fuente, lo cortas o lo divides en dos líneas?
- ¿Cómo nombras los archivos para que no se repitan si hay dos personas con el mismo nombre?
Casos de prueba
- Un nombre de 40 caracteres cabe en el carné.
- Nombres con tildes y "ñ" se ven bien (la fuente debe tener esos caracteres).
- Una fila del CSV sin mesa se reporta en lugar de generar un carné incompleto.
🏫 Usa datos ficticios para las pruebas; los datos reales de estudiantes solo con autorización.
9. 🏗️ Mi Portafolio Profesional — Etapa 4
(Proyecto integrador creado para esta adaptación; etapas anteriores en los Días 82 a 84.)
Etapa 4 (hoy):
- Repositorio de la app de marcas de agua con
marcas.py,app.py,requirements.txt(conPillow) y.gitignore(excluye imágenes de prueba personales). README.mdcon una captura de la interfaz y un antes/después con una imagen libre de derechos o propia.DIARIO.mddel Día 85.- Ficha en
proyectos.jsony comprobación en tu sitio. Ya tienes proyectos de consola y de GUI: revisa que las tarjetas de tu sitio muestren la categoría o las tecnologías.
10. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Qué es el canal alfa? ¿Por qué el JPEG no sirve para guardar transparencia?
- ¿Por qué conviene separar la lógica de las imágenes de la interfaz?
- ¿Qué diferencia hay entre
resizeythumbnail? - ¿Por qué hay que guardar una referencia a
PhotoImageen Tkinter?
Análisis de código
tamano = max(12, imagen.width // 20)¿Qué tamaño de letra resulta para imágenes de 2000, 400 y 100 px de ancho? ¿Para qué sirve max?
Predicción
Una imagen de 1000 × 500 se procesa con thumbnail((500, 500)). ¿Qué tamaño queda? ¿Y con resize((500, 500))?
Depuración
"Mi marca debería verse semitransparente, pero se ve totalmente blanca y opaca." ¿Qué paso de la sección 3 falta probablemente?
Modificación
Cambia agregar_marca_texto para que reciba un parámetro esquina ("sup_izq", "sup_der", "inf_izq", "inf_der", "centro") y calcule la posición según él.
Creación
Escribe nombre_salida(ruta) que reciba "fotos/playa.png" y devuelva "fotos/playa_marca.png" (pista: pathlib.Path, atributos .stem y .suffix).
11. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú primero → consulta a la IA sobre un punto concreto → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — Código desactualizado. Pide a una IA "cómo redimensionar una imagen con Pillow manteniendo la calidad". Si la respuesta usa ANTIALIAS o textsize, ejecútala, lee el error y corrígela con la documentación oficial de Pillow ("Deprecations and removals"). Anota qué versión de Pillow los eliminó.
Actividad 2 — Explicar para entender. Pide a una IA que explique Image.alpha_composite y luego comprueba su explicación con un experimento: compón una capa roja con alfa 0, 128 y 255 sobre una imagen azul y observa los resultados.
Actividad 3 — Revisar la interfaz. Describe tu ventana (sin pegar datos personales) y pide sugerencias de usabilidad. Aplica solo las que puedas justificar.
Declara el uso: sección "Uso de IA" en el README.md.
12. 🛠️ Observaciones sobre el material original
| Archivo | Observación | Tratamiento |
|---|---|---|
dia85-tarea4.md |
Menciona Pillow sin versión; muchos tutoriales en línea usan APIs eliminadas en Pillow 10 (ANTIALIAS, textsize) |
Se usa la API vigente y se advierte en errores frecuentes y en la actividad 1 de IA |
dia85-tarea4.md |
El caso de uso (publicar fotos) no habla de derechos de autor ni de metadatos | Sección 6 |
dia85-tarea4.md |
Remite a un servicio en línea (watermarkly.com) como referencia | Se conserva como inspiración; no es necesario crear cuenta |
dia85-tarea4.md |
"y algo de Google" | Sección 3 y 4 explican los conceptos que habría que buscar |
13. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| App de escritorio con Tkinter para cargar una imagen | dia85-tarea4.md |
Sección 5 | App de marca de agua | Reto 3 (por lotes) | Secciones 7 y 9 |
| Agregar logo o texto como marca de agua con Python (Pillow) | dia85-tarea4.md |
Secciones 3 y 4 | App de marca de agua | Retos 1, 2 y 4 | Sección 7 |
| Caso de uso: publicar fotos con tu sitio web | dia85-tarea4.md |
Secciones 2 y 6 | — | — | Sección 7 (contexto) |
| Recursos: Pillow, documentación de tkinter, Google | dia85-tarea4.md |
Secciones 4, 5 y 11 | — | — | — |
| Momento de reflexión | dia85-tarea4.md |
Sección 7 (versión final) | Pregunta de reflexión | — | Sección 9 |
| (No existe en el curso original) | — | Derechos de autor, EXIF | — | Carnés para un evento | Sección 8 |
14. 📦 Evidencias de aprendizaje del Día 85
- La aplicación funcionando: capturas de la interfaz y de un antes/después.
- La prueba automática de la lógica superada.
- La tabla de pruebas de la sección 7 con resultados.
- El
DIARIO.mddel Día 85, el repositorio y la ficha en el portafolio. - Cuaderno de errores: RGBA en JPEG,
ANTIALIAS/textsizeeliminados, fuente no encontrada, logo sin máscara,PhotoImagesin referencia, original sobrescrito, imagen deformada conresize.