# Cómo comprimir PNG localmente hasta un 80 %

Crea un compresor local con pngquant y OxiPNG para reducir imágenes desde Linux, un VPS o Termux sin depender de herramientas web.

- URL: https://miguelramos.net/blog/comprimir-png-localmente-pngquant-oxipng/
- Fecha: 2026-09-08
- Autor: Miguel Ramos
- Tags: Rendimiento, Open Source, Infraestructura

---
Todo comenzó con una contradicción pequeña pero constante: las portadas del blog se veían bien, pero pesaban demasiado. Cada nueva imagen repetía el mismo proceso de abrir una herramienta web, subir el archivo, esperar, descargar otra copia y comprobar si la calidad seguía intacta.

El problema no era comprimir una imagen. Era depender de ese recorrido cada vez que aparecía un PNG nuevo.

**La solución terminó siendo un pequeño laboratorio local con dos etapas: `pngquant` reduce la complejidad visual y OxiPNG reorganiza el archivo resultante para ocupar todavía menos espacio.** El mismo proceso pudo vivir en un VPS Linux y dentro de Termux, directamente en Android.

El ahorro llegó a rondar entre el 60 y el 80 % en imágenes adecuadas. No fue una constante ni una promesa matemática. Cada PNG reaccionó según sus colores, transparencias, degradados y optimización previa. Lo interesante no fue alcanzar una cifra exacta, sino descubrir dónde desaparecían realmente los bytes.

:::important[El hallazgo]
La mayor reducción no ocurre cuando OxiPNG reorganiza el archivo. Ocurre antes, cuando `pngquant` decide qué información de color puede descartar sin degradar visiblemente la imagen. OxiPNG completa el trabajo.
:::

## El peso del PNG ocultaba dos problemas distintos

**Un PNG puede ser pesado por la cantidad de información visual que conserva y por la forma en que esa información está codificada.** Tratar ambas causas como si fueran una sola conduce a resultados limitados.

La primera herramienta del recorrido, [`pngquant`](https://github.com/kornelski/pngquant), transforma imágenes PNG de 24 o 32 bits en archivos de paleta de 8 bits con canal alfa. Para hacerlo analiza los colores, construye una paleta optimizada y vuelve a asignar los píxeles. Sus mantenedores indican que este proceso produce con frecuencia archivos entre un 60 y un 80 % más pequeños.

La segunda, [`oxipng`](https://github.com/oxipng/oxipng), no intenta repetir esa cuantización. Explora filtros y estrategias de compresión para encontrar una representación más eficiente del mismo PNG. Está escrita en Rust, trabaja en paralelo y su optimización puede conservar intacta la representación visual.

| Etapa | Qué transforma | Naturaleza del cambio |
|---|---|---|
| `pngquant` | La cantidad y distribución de colores | Pérdida visual controlada |
| `oxipng` | La estructura y codificación del PNG | Sin pérdida visual en este flujo |

::github{repo="kornelski/pngquant"}

::github{repo="oxipng/oxipng"}

Vistas juntas, las herramientas forman una cadena y no una competencia. La primera simplifica el contenido; la segunda busca la mejor manera de empaquetar esa versión simplificada.

## La primera transformación ocurrió dentro de la paleta

**`pngquant` consiguió la reducción grande porque dejó de tratar cada posible color como indispensable.**

Una portada con sombras, reflejos y degradados puede contener miles de variaciones muy próximas entre sí. Muchas existen en los datos del archivo, aunque el ojo apenas pueda distinguirlas. La cuantización agrupa esas variaciones alrededor de una paleta más pequeña.

El rango elegido fue:

```bash
pngquant \
    --quality=70-90 \
    --speed=1 \
    --strip \
    --force \
    --output "$output" \
    "$input"
```

`--quality=70-90` no ordena que el archivo final pese entre el 70 y el 90 % del original. El límite superior es la calidad que `pngquant` intenta alcanzar; el inferior es el umbral que se niega a cruzar. Si el resultado cae por debajo del mínimo, la herramienta no guarda la conversión y termina con el código `99`.

La opción `--speed=1` dedica más trabajo a buscar una paleta eficiente. El proyecto establece `4` como equilibrio predeterminado y aconseja conservarlo en la mayoría de escenarios. Aquí elegí `1` porque las portadas finales se procesan una sola vez: algunos segundos adicionales importan menos que conservar calidad y obtener un archivo pequeño.

:::note[La reducción depende de la imagen]
Un icono plano con pocos colores puede reducirse de forma extraordinaria. Una portada 3D con niebla, sombras suaves y transparencias puede mostrar bandas si la calidad se fuerza demasiado. El porcentaje de ahorro nunca debe sustituir la inspección visual.
:::

`--strip` añadió otra decisión: no copiar chunks opcionales del PNG. Eso elimina metadatos y reduce algunos bytes más, pero también puede retirar información de procedencia. Esa consecuencia aparecería de nuevo al final del experimento.

## OxiPNG encontró espacio donde ya no parecía haberlo

**Después de la cuantización, OxiPNG todavía pudo reducir el archivo sin volver a modificar su apariencia.**

La segunda etapa fue más silenciosa:

```bash
oxipng \
    -o 4 \
    --strip safe \
    "$output"
```

El nivel `-o 4` explora un conjunto intermedio-alto de posibilidades. Subir más puede aumentar considerablemente el tiempo a cambio de reducciones cada vez menores. `--strip safe` elimina metadatos considerados prescindibles para representar la imagen.

No añadí `--alpha`. La documentación presenta esa opción dentro de una configuración útil para la web, pero puede modificar los valores RGB ocultos en píxeles completamente transparentes. Esos valores normalmente no se ven, aunque preferí no tocarlos.

:::tip[Dos etapas, dos preguntas]
Si quieres saber por qué una imagen perdió tanto peso, compara tres archivos: el original, la salida inmediata de `pngquant` y el resultado después de OxiPNG. Así puedes atribuir el ahorro a cada transformación en lugar de observar solamente el número final.
:::

## El mismo flujo tuvo que sobrevivir en dos mundos Linux

**El script Bash era portátil; los binarios no necesariamente lo eran.** Esa diferencia quedó clara cuando el proceso salió del VPS y llegó a Termux.

En Fedora y Ubuntu, la ruta más estable fue comenzar por los repositorios de la distribución:

```bash
# Fedora
sudo dnf install pngquant oxipng

# Ubuntu o Debian
sudo apt update
sudo apt install pngquant oxipng
```

Si OxiPNG no está disponible o su versión es demasiado antigua, el siguiente paso es revisar sus [releases oficiales](https://github.com/oxipng/oxipng/releases), identificar la arquitectura con `uname -m` y descargar el asset correspondiente. Para un servidor `x86_64`, el nombre esperado contiene:

```text
x86_64-unknown-linux-musl.tar.gz
```

El binario puede instalarse en la ruta común del sistema:

```bash
tar -xzf oxipng-*-x86_64-unknown-linux-musl.tar.gz
sudo install -m 0755 oxipng-*/oxipng /usr/local/bin/oxipng
oxipng --version
```

La versión no debe quedar incrustada para siempre en un tutorial. La página de releases es la fuente para comprobar el número actual, la arquitectura disponible y, cuando el proyecto los publique, los checksums.

## Termux reveló que Android no es otra distribución de escritorio

**Termux comparte el kernel de Android, pero su espacio de usuario, cargador y biblioteca C no son los de Ubuntu o Fedora.**

Los paquetes de Termux se construyen normalmente contra `bionic`, la biblioteca C de Android. Un ejecutable dinámico preparado para `glibc` puede tener la arquitectura correcta y aun así fallar porque busca un cargador que no existe en el teléfono. La [documentación del entorno de ejecución de Termux](https://github.com/termux/termux-packages/wiki/Termux-execution-environment) explica esta frontera.

`pngquant` no presentó misterio porque ya existía como paquete nativo:

```bash
pkg update
pkg install pngquant -y
pngquant --version
```

OxiPNG sí convirtió la instalación en parte del descubrimiento. `cargo install oxipng` falló durante la compilación de una dependencia nativa. Sin conservar y estudiar el registro completo, no sería correcto culpar exclusivamente a `zopfli`, al NDK o a `bionic`.

La salida práctica fue un release estático para `aarch64-unknown-linux-musl`. En la prueba original se utilizó la versión `10.2.1`; al repetirla debe sustituirse por la versión publicada en el [release más reciente](https://github.com/oxipng/oxipng/releases/latest).

```bash
uname -m
cd "$PREFIX/bin"

curl -fLO \
  https://github.com/oxipng/oxipng/releases/download/v10.2.1/oxipng-10.2.1-aarch64-unknown-linux-musl.tar.gz

tar -xzf oxipng-10.2.1-aarch64-unknown-linux-musl.tar.gz \
  --strip-components=1 \
  --wildcards '*/oxipng'

chmod 0755 oxipng
rm oxipng-10.2.1-aarch64-unknown-linux-musl.tar.gz
oxipng --version
```

`curl -f` cumple una función pequeña pero importante: si GitHub responde con un error HTTP, la descarga falla en vez de guardar una página HTML bajo el nombre de un archivo comprimido.

:::caution[Compatible aquí no significa universal]
Que un binario estático con `musl` haya funcionado en este dispositivo no demuestra que cualquier ejecutable Linux funcione en Android. Deben coincidir la arquitectura, las llamadas al sistema, las expectativas del programa y las políticas del dispositivo.
:::

El destino también importó. `$PREFIX/bin` es la ubicación de ejecutables de Termux, según su [estructura del sistema de archivos](https://github.com/termux/termux-packages/wiki/Termux-file-system-layout). `/sdcard` sirve para leer y guardar imágenes, pero sus restricciones impiden tratarlo como un directorio Unix normal para ejecutar programas.

## El experimento terminó convertido en un solo comando

**Cuando ambos binarios estuvieron disponibles, toda la investigación quedó condensada en un script pequeño y repetible.**

Guárdalo como `~/comprimir-png.sh`:

```bash
#!/usr/bin/env bash

set -Eeuo pipefail

if [[ $# -lt 1 || $# -gt 2 ]]; then
    echo "Uso: $0 imagen.png [salida.png]"
    exit 1
fi

input=$1

if [[ ! -f "$input" ]]; then
    echo "Error: no existe el archivo: $input"
    exit 1
fi

if [[ "${input,,}" != *.png ]]; then
    echo "Error: el archivo debe tener extensión .png"
    exit 1
fi

if [[ $# -eq 2 ]]; then
    output=$2
else
    filename=$(basename "$input")
    directory=$(dirname "$input")
    name=${filename%.*}
    output="$directory/${name}-comprimida.png"
fi

if [[ "$input" == "$output" ]]; then
    echo "Error: la salida no puede sobrescribir el original"
    exit 1
fi

for command_name in pngquant oxipng; do
    if ! command -v "$command_name" >/dev/null 2>&1; then
        echo "Error: $command_name no está instalado"
        exit 1
    fi
done

original_size=$(stat --format='%s' "$input")

echo "Cuantizando colores..."
pngquant \
    --quality=70-90 \
    --speed=1 \
    --strip \
    --force \
    --output "$output" \
    "$input"

echo "Aplicando optimización final..."
oxipng -o 4 --strip safe "$output"

compressed_size=$(stat --format='%s' "$output")

if (( original_size > 0 )); then
    reduction=$((100 - compressed_size * 100 / original_size))
else
    reduction=0
fi

echo
echo "Original:   $(numfmt --to=iec-i --suffix=B "$original_size")"
echo "Comprimido: $(numfmt --to=iec-i --suffix=B "$compressed_size")"
echo "Reducción:  ${reduction}%"
echo "Resultado:  $output"
```

Después de marcarlo como ejecutable:

```bash
chmod +x ~/comprimir-png.sh
```

el recorrido completo cabe en una línea:

```bash
~/comprimir-png.sh portada.png
```

El original permanece intacto y el resultado aparece como `portada-comprimida.png`. También puede elegirse otro destino:

```bash
~/comprimir-png.sh portada.png portada-optimizada.png
```

En Termux, después de conceder acceso al almacenamiento compartido con `termux-setup-storage`, la misma herramienta alcanza las Descargas de Android:

```bash
~/comprimir-png.sh /sdcard/Download/imagen.png
```

## La automatización expuso que no existe un preset universal

**Cuando el script comenzó a procesar imágenes distintas, el porcentaje dejó de ser la métrica principal y la calidad visual tomó su lugar.**

| Tipo de imagen | Rango inicial sugerido | Qué conviene vigilar |
|---|---|---|
| Portadas 3D, fotografías y degradados | `80-95` | Bandas en sombras y luces suaves |
| Imágenes generales para web | `70-90` | Variaciones en colores cercanos |
| Iconos y gráficos planos | `50-80` | Bordes y transparencias degradadas |

Estos rangos son puntos de partida, no leyes. Una imagen con ocho colores no necesita la misma tolerancia que una portada con niebla, piel o reflejos. El original debe conservarse y la copia debe revisarse a tamaño completo sobre fondos claros y oscuros.

El procesamiento por lotes llegó después, cuando una sola imagen dejó de ser el caso normal:

```bash
find src/assets/blog \
  -type f \
  -iname '*.png' \
  ! -iname '*-comprimida.png' \
  -print0 |
while IFS= read -r -d '' image; do
    ~/comprimir-png.sh "$image"
done
```

`-print0` conserva correctamente los nombres con espacios. La exclusión de `*-comprimida.png` evita que una ejecución futura vuelva a comprimir sus propios resultados.

## Al quitar metadatos también puede desaparecer la procedencia

**La reducción del archivo tiene un costo menos visible: los chunks opcionales pueden contener información que no participa en la imagen, pero sí en su historia.**

La [especificación C2PA](https://spec.c2pa.org/specifications/specifications/2.4/specs/C2PA_Specification.html) establece que un manifiesto embebido en PNG puede almacenarse en el chunk auxiliar privado `caBX`. Al reconstruir la imagen y descartar chunks opcionales, esa credencial normalmente deja de acompañar a la copia transformada y su firma ya no puede validarla.

:::warning[Comprimir también es transformar]
Si un flujo necesita conservar Content Credentials, no debe aplicar `--strip` sin una estrategia posterior de verificación y nueva firma. Que los píxeles se vean iguales no significa que el archivo conserve la misma procedencia criptográfica.
:::

Esto tampoco garantiza que desaparezca cualquier mecanismo de procedencia. Pueden existir manifiestos externos, registros remotos o marcas resistentes incorporadas en los píxeles. El script optimiza un PNG; no es una herramienta para borrar rastros.

## El resultado fue más que un archivo pequeño

**El verdadero resultado fue convertir una tarea web repetitiva en una operación local, observable y automatizable.**

El viaje del PNG dejó varias conclusiones:

- `pngquant` produce la reducción principal al simplificar la paleta.
- OxiPNG aprovecha la estructura resultante sin repetir la pérdida visual.
- El script Bash puede ser el mismo en Linux y Termux.
- Los ejecutables dependen de la arquitectura y del entorno donde correrán.
- El ahorro debe evaluarse junto con la calidad y los metadatos conservados.
- La automatización vale más cuando mantiene intacto el original.

Antes de recurrir a capas de compatibilidad o implementaciones alternativas de `glibc` en Termux, el orden razonable es buscar el paquete con `pkg`, comprobar releases oficiales compatibles, verificar arquitectura y procedencia, y solo entonces considerar una compilación específica para Android o un entorno `proot`.

Al final, una portada dejó de viajar a una página web externa. Ahora atraviesa dos herramientas locales, conserva su original y regresa más ligera con una explicación medible de lo que ocurrió. Ese pequeño cambio resume una idea que aplico con frecuencia: **las mejores automatizaciones no esconden el proceso; lo vuelven repetible, verificable y propio.**