Redimensionando y Aplicando una Marca de Agua a imágenes usando Python y PIL

Redimensionando y Aplicando una Marca de Agua a imágenes usando Python y PIL

Hola a todos,

Hoy, estaremos viendo cómo redimensionar y aplicar una marca de agua a imágenes usando Python y la librería PIL. Este código es sencillo luego de haber pasado por algunos problemas iniciales y un mensaje de deprecación. Este código está basado en el código de la siguiente página:

Recientemente decidí darle una oportunidad a Linux y ando buscando software alternativos a esos que he usado en Windows. Para aplicar una marca de agua y disminuir las dimensiones de mis imágenes, estaba usando IrfanView. Este programa es sólamente para Windows, pero su FAQ dice que se puede usar en Linux usando WINE.

En Linux, tenemos también el «suite» de ImageMagick, que es utilizado para manipular imágenes. Encontré este complicado de usar, por lo que decidí usar Python para escribir un «script» (programa) para realizar este proceso.

Python contiene la increible librería PIL, que es usado para trabajar con imágenes. Una ventaga de esto es que podemos actualizar nuestro script en el futuro para realizar otras tareas. En mi código, estoy realizando las siguientes 3 tareas:

  1. Rotar la imagen usando la información EXIF incluída en el archivo.
  2. Redimensionar/Disminuir las dimensiones de la imagen.
  3. Aplicar la marca de agua basado en un texto.

El primer paso es necesario porque de lo contrario, PIL ignorará la orientación de la imagen y procederá a los pasos 2 y 3. El resultado será una imagen que tiene su marca de agua colocada correctamente pero la orientación de la imagen está mal. Esto sucede porque nuestros teléfonos y cámaras digitales toman las fotos guardando la rotación de la misma en el «metadata» de la imagen. Este «metadata» se llama EXIF. La imagen de por sí no está rotada, y es la aplicación la encargada de leer esta información y rotarla adecuadamente. Nota que ninguno de estos pass mencionados afectará la imagen fuente.

Otras librerías de Python que usaremos son os y glob.

  • os será usada para obtener el nombre del archivo y extraer la extensión.
  • glob será usada para obtener una lista de archivos JPEG (con la extensión *.jpg) que se encuentren en la carpeta que contiene nuestras fotos.

Ahora, comencemos con el código.

Paso 1: Importando las librerías en Python

Antes de comenzar a escribir nuestro código, debemos importar algunas librerías que nos permitirán realizar las operaciones en nuestras imágenes. Esto es muy importante. De lo contrario, Python fallará en la ejecución del código.

import os
import glob
from PIL import Image, ImageDraw, ImageFont, ImageOps

Los primeros 2 «imports» son my directos. El 3er «import» importará algunas clases específicas de la librería PIL.

  • Image es la clase principal responsable de abrir y guardar las imágenes.
  • ImageDraw nos permite dibujar en la imagen. Estaremos añadiendo un texto, y es por esto que necesitamos esta clase.
  • ImageFont nos permitirá utilizar un tipo de letra instalado en nuestro sistema en formato TrueType.
  • ImageOps nos permite realizar operaciones en la imagen. Estaremos usando esta clase para rotar y redimensionar nuestra imagen.

Paso 2: Creando la función para procesar las imágenes

Estaremos definiendo la función que estará a cargo de ejecutar los pasos mencionados anteriormente. Esta función también se encargará de extraer y generar el nombre del archivo a leer y escribir usando la librería os. Llamaremos esta función resize_and_watermark:

def resize_and_watermark(file: str, path: str) -> None:
    print("Resizing and Watermarking file {}".format(file))
    filename = os.path.basename(file)
    filename_without_extension, extension = os.path.splitext(filename)
    output_filename = "{}-out-watermark{}".format(filename_without_extension, extension)
  1. En la linea def, estamos pasando 2 argumentos: El nombre del archivo file como un «string» (str) , y la dirección de la carpeta path también como un «string». Esto lo hacemos para poder leer y guardar el archivo procesado. Esta función no devuelve nada, por lo que su salida es tipo None (Nada).
  2. Luego, imprimimos un mensaje con el nombre del archivo que estamos procesando (linea 2).
  3. En las próximas 3 líneas, estamos extrayendo el nombre del archivo de la dirección entera (os.path.basename).
  4. Luego, procedemos a dividir la extensión del archivo con os.path.splitext. Esto nos devuelve 2 strings: El primero es el nombre del archivo sin la extensión, y el 2do es la extensión del archivo.
  5. La última línea formatea el nombre del archivo de salida, en donde añadimos out-watermark y la extensión del archivo al mismo. Nota que estamos pasando el nombre sin extensión y la extensión a la función .format.

Paso 3: Abriendo la imagen, rotándola, y redimensionandola.

Ahora que tenemos el nombre de archivo, nos toca añadir el código para abrir la imagen, rotarla de acuerdo a la información en su metadata EXIF, y redimensionar la imagen de acuerdo al ancho y alto establecido en pixels en el código.

    img = Image.open(file)
    img = ImageOps.exif_transpose(img)
    img = ImageOps.contain(img, (1920, 1080))

La primera línea abre la imagen y la guarda en la variable img. Esta variable mantendrá la imagen mientras la vamos procesando.

Si la imagen contiene la orientación almacenada en su metadata EIF, ésta será utilziada para rotar nuestras imágenes antes de realizar alguna otra oepración. La línea ImageOps.exif_transpose(img) se encarga de esto. De lo contrario, tendremos imágenes sin rotar como esta:

Donuts - Not Rotated

Una imagen rotada correctamente se verá como la siguiente:

Donuts - Rotated

La línea ImageOps.contain(img, (1920, 1080)) se encargará de disminuir la imagen a una resolución de 1920×1080 píxeles manteniendo su relación de aspecto. Esta función también nos ayuda a mantener el tamaño de imagen pequeño pues las dimensiones de la imagen resultante serán más pequeño que la imagen original, asumiendo que la original es mayor que 1920×1080. Podemos cambiar estos números a otra dimension que queramos, pero tenemos que mantener el formato (x, y) y los números deben representar píxeles.

En este momento, hemos rotado y reducido las dimensiones de nuestra imagen.

Paso 4: Dibujando el texto

Ahora nos toca poner el texto en la imagen. Esta parte es más complicada que las anteriores, por lo que iremos en más detalles a continuación:

    draw = ImageDraw.Draw(img)
    text = "https://moisescardona.me"
    font = ImageFont.truetype('/home/moisespr123/.local/share/fonts/micross.ttf', 48)
    textbbox = draw.textbbox((0, 0), text, font)

En la línea 14, tenemos una variable llamada draw que contiene la data de la imagen y nos permitirá modificar el objeto img directamente.

En la línea 15, tenemos la variable text que símplemente tiene el texto que queremos poner en la imagen.

En la línea 16, tenemos una variable llamada font la cual almacena el tipo de letra y tamaño haciendo uso de la clase ImageFont y su función truetype. El primer argumento es la dirección hacia el tipo de letra TrueType que queremos usar y el 2do argumento es el tamaño de la letra.

Finalmente, en la línea 17, dibujamos el texto en los píxeles (0, 0). La variable textbbox guardará un «slice» con el tamaño del texto en las 4 esquinas.

Podemos imprimir esta información facil usando print:

    print(textbbox)
    # prints the following
    #  (0, 11, 550, 55)

Estos son los píxeles del resultado del texto. Sólo necesitamos los últimos 2 números pues vamos a restar las dimensiones de la imagen con estos números para colocar el texto correctamente en la imagen.

Esto nos lleva a la siguiente pieza del código:

    textwidth = textbbox[2]
    textheight = textbbox[3]
    width, height = img.size
    x = width - textwidth - 10
    y = height - textheight - 10

Las primeras 2 líneas contienen los últimos 2 números de la variable textbbox que son el ancho y alto del texto, respectivamente. No necesitamos los primeros 2 números en este caso, pues colocaremos el texto/marca de agua en la esquina inferior derecha.

La línea 20 obteiene el ancho y alto de la imagen ya redimensionada.

Las últimas 2 líneas entonces restan el ancho de la imagen con el ancho del texto, y el alto de la imagen con el alto del texto. A estos les estoy restando 10 píxeles más para marginarlo mejor. Esto nos resulta en la posición x y y en donde colocaremos el texto en la imagen.

Podemos entonces dibujarlo en la imagen con la siguiente línea:

    draw.text((x, y), text, font=font, stroke_width=3, stroke_fill="#000")

draw.text es la función que se usa para dibujar un texto en la imagen.

  1. El primer argumento es la posición x y y para colorar el texto. Estos números se basan en la coordineda superior-izquierda ya que el sistema de coordineda de PIL Comienza en esa parte.
  2. La variable text tiene el texto que escribimos en la línea 15.
  3. font=font pasa la variable font de la línea 16 al argumento font para así decirle a esta función qué tipo de letra y tamaño usar al dibujar.
  4. stroke_with=3 es el tamaño para usar en el borde de la letra. 3 es el tamaño a usar.
  5. stroke_fill="#000" es el color a usar para el borde. #000 es el color negro en el sistema hexadecimal.

En este punto, ya hemos dibujado el texto en la imagen y sólo nos toca guardarla.

    img.save(path + "/" + output_filename)

Esta línea es sencilla y se explica por sí sola. La misma guardará la imagen del objecto img a la dirección del archivo resultante generado por el código. Nota que estamos pasando el directorio de la variable path y la variable output_filename que contiene el nombre del archivo generado anteriormente.

Ahora, sólo nos toca escribir una función para recorrer y llamar esta función para cada archivo de la carpeta a usar.

Paso 5: Procesando cada imagen en el directorio

Para procesar cada imagen JPEG en una carpeta, tenemos que escribir una función que recorrerá una lista de archivos y llamará la función que hemos escrito en los pasos anteriores. Para esto, definiremos una variable path que contendrá la dirección de la carpeta a usar, y obtendremos la lista de archivos usando glob.glob.

if __name__ == "__main__":
    path = '/home/moisespr123/Donuts'
    for filepath in glob.glob(path + '/*.jpg'):
        resize_and_watermark(filepath, path)
    print("Done Resizing and Watermarking files.")

Este código también actúa como la función «main» que es usual tener en un programa. Es por eso que tenemos la primera línea if __name__ == "__main__": De esta manera, se ejecuta este código al llamar este script en el terminal.

Lo próximo que vemos es la variable path que almacena la carpeta a usar.

Ahora es donde vamos a recorrer y ejecutar la función en cada archivo que la función glob.glob encuentre. Aquí, vemos que tenemos la variable path y con un asterisco * indicamos que busque todos los archivos cuya extensión termine en .jpg.

Para cada imagen JPEG con la extensión *.jpg, correremos la función resize_and_watermark pasando la dirección completa del archivo (filepath) y la dirección de la carpeta (path) como argumentos.

Una vez hayamos procesado cada archivo de la carpeta, salimos de la recursión for e imprimimos el mensaje Done Resizing and Watermarking files.

Corriendo el script.

Ahora, estamos listos para ejecutar el script. Aquí vemos la carpeta a usar antes de correrlo.

Python Resize and Watermark Script - Folder before running the script

Para ejecutar el script, simplemente abrimos una ventana del terminar, vamos a la carpeta que contiene el script, y escribimos python3 seguido por el nombre del script:

Python Resize and Watermark Script - Running the Script

Como observamos, el script corrió, pero nos dió una advertencia. Veremos cómo desactivarlo más abajo. Ese mensaje no afecta la salida, pero lo verás por defecto cuando proceses archivos grandes. A pesar de esto, el script corrió exitosamente y aquí tenemos el resultado:

Donuts - Watermarked

La marca de agua está localizado en la parte inferior derecha. Es muy simple e ideal para nuestras imágenes.

Aquí tenemos la carpeta con la imagen procesada. Nota que el tamaño de la imagen también es más pequeña que la imagen original:

Python Resize and Watermark Script - Folder after running the script

Podemos ver que la imagen siguí el límite establecido en el paso de la redimensión pes el archivo tiene un alto de 1080 píxeles:

Python Resize and Watermark Script - Image dimensions

¡Felicidades! ¡Haz reducido y aplicado una marca de agua en la imagen!

Congratulations! We have successfully resized and watermarked our image!

Bono: Desactivando el mensaje DecompressionBombWarning

Debido a que la imagen que procesé tiene una resolución de 108MP, exede un límite de píxeles que PIL verifica. Sólo nos advierte sobre esto y podemos ignorarlo, pero también podemos desactivar este mensaje en el código añadiendo la siguiente línea:

    Image.MAX_IMAGE_PIXELS = None

Ahora, cuando corremos el programa otra vez, el mensaje no aparece:

Python Resize and Watermark Script - Running the Script - No Warning

¡Felicidades! Hemos escrito un script/programa en Python para redimensionar y aplicar una marca de agua. Espero que les haya gustado este contenido y esperen ver más contenido similar.

A continuación tenemos el código completo. Noten que deben cambiar las siguientes variables:

  • text a tu texto preferido.
  • font a la localiación del tipo de letra TrueType y el tamaño de letra a usar.
  • path a la dirección de la carpeta que contenga los archivos JPEG.

Opcionalmente, también puedes cambiar los valores 1920 y 1080 de la línea 13 para reducir la imagen a otra dimensión. La localización de la marca de agua también se puede cambiar ajustando los valores x y y de las líneas 21 y 22, respectivamente.

import os
import glob
from PIL import Image, ImageDraw, ImageFont, ImageOps


def resize_and_watermark(file: str, path: str) -> None:
    print("Resizing and Watermarking file {}".format(file))
    filename = os.path.basename(file)
    filename_without_extension, extension = os.path.splitext(filename)
    output_filename = "{}-out-watermark{}".format(filename_without_extension, extension)
    img = Image.open(file)
    img = ImageOps.exif_transpose(img)
    img = ImageOps.contain(img, (1920, 1080))
    draw = ImageDraw.Draw(img)
    text = "Your Text"
    font = ImageFont.truetype('/path/to/truetype/font.ttf', 48)
    textbbox = draw.textbbox((0, 0), text, font)
    textwidth = textbbox[2]
    textheight = textbbox[3]
    width, height = img.size
    x = width - textwidth - 10
    y = height - textheight - 10
    draw.text((x, y), text, font=font, stroke_width=3, stroke_fill="#000")
    img.save(path + "/" + output_filename)


if __name__ == "__main__":
    Image.MAX_IMAGE_PIXELS = None
    path = '/path/to/jpg/files'
    for filepath in glob.glob(path + '/*.jpg'):
        resize_and_watermark(filepath, path)
    print("Done Resizing and Watermarking files.")

FAQ

  • Q: Recibo un mensaje de error indicando que PIL no se puede importar
    A: Asegúrate de instalar la libreria Pillow usando pip3 install -U Pillow.
Python Resize and Watermark Script - Install Pillow