Obtener el ancho y alto, escalar y convertir a otro formato imágenes con Java

Escrito por el .
java planeta-codigo
Enlace permanente Comentarios

Las aplicaciones que tratan con imágenes es posible que necesiten conocer algunos datos de la imagen como anchura y altura, realizar algunas operaciones de manipulación básicas como escalado y conversión entre formatos. El propio JDK de Java ofrece algunas clases y soporta varios de los formatos más comunes de imagen. Para usos más avanzados hay que recurrir a librerías y si no fueran suficientes a comandos del sistema más avanzados.

Java

El lenguaje de programación Java es un lenguaje de propósito general con el que es posible realizar cualquier tarea. Sin embargo, Java como lenguaje o plataforma aparte de Android el mayor uso que se le da es para aplicaciones y servicios en el lado del servidor.

Una de las necesidades que pueden surgir es escalar una imagen o cambiarle de formato, aunque Java solo ofrece un soporte básico para el tratamiento de imágenes, hay varias librerías pero pocas bien mantenidas y con buen soporte, si ni una librería ofrece la funcionalidad deseada en el caso más extremo se puede invocar a un proceso del sistema que ofrezca la funcionalidad com ImageMagick o FFmpeg.

La conversión y cambio de formato es un proceso costoso en CPU y memoria ya que requieren cargar la imagen en memoria más si las imágenes a tratar son muy grandes, de varias decenas de megabytes.

El propio JDK ofrece en su API soporte para los formatos de imágenes más comunes y algunas manipulaciones de imagen, hay varias librerías Java para la manipulación de imágenes y finalmente están los comandos del sistema.

Formatos de imágenes

Hay varios formatos de imágenes que se pueden categorizar en dos grandes grupos: imágenes sin pérdida o con pérdida. Los formatos sin pérdida son más fieles a la imagen original pero con mayor tamaño que la equivalente en un formato con pérdida de calidad, en las imágenes con pérdida de calidad los algoritmos tratan de que visualmente la pérdida sea lo menos apreciable posible con un tamaño de imagen significativamente menor.

El formato png es un formato sin pérdida que ofrece buenos resultados para capturas de pantalla del ordenador. Para fotos el formato jpg ofrece un archivo de tamaño significativamente más reducido con una calidad bastante fiel al original.

Más recientemente han surgido nuevos formatos de imagen que utilizan otros algoritmos para la reducción de tamaño heic y heif utilizan el algoritmo con coste de licencia de x265 y el formato sin coste de licencia webp que ofrece similar o mejor resultado que el x265 que además soporta imágenes sin pérdida.

Procesar imágenes con Java

En el propio JDK hay varias clases para la utilización de imágenes en el paquete java.awt.

Formatos de imagen soportados

Los formatos de imagen soportados se pueden obtener mediante código y son extensibles añadiendo librerías al classpath, es posible que un formato se soporte en modo lectura pero no en escritura.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package io.github.picodotdev.blogbitix.javaimageprocess;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;

import javax.imageio.ImageIO;

import com.twelvemonkeys.image.ResampleOp;
import net.coobird.thumbnailator.Thumbnails;

public class Main {

    public static void main(String[] args) throws Exception {
        printSupportedFormats();

        BufferedImage image = readImage(Main.class.getResourceAsStream("/gnome.jpg"));
        BufferedImage imageWebp = readImage(Main.class.getResourceAsStream("/gnome.webp"));

        BufferedImage scaledImage = scaleJava(image);
        convertJava(scaledImage, ImageFormat.JPG, new FileOutputStream("gnome-scaled-java.jpg"));

        scaleThumbnailator(Main.class.getResourceAsStream("/gnome.jpg"), new FileOutputStream("gnome-scaled-thumbnailator.jpg"));
        scaleImageMagick(Main.class.getResourceAsStream("/gnome.jpg"), new FileOutputStream("gnome-scaled-imagemagick.jpg"));
        scaleTwelvemonkeys(readImage(Main.class.getResourceAsStream("/gnome.jpg")), new FileOutputStream("gnome-scaled-twelvemonkeys.jpg"));

        printImageWidthHeight("original", image);
        printImageWidthHeight("original webp", imageWebp);
        printImageWidthHeight("scaled", scaledImage);
        printImageWidthHeight("scaled thumbnailator", readImage(new FileInputStream("gnome-scaled-thumbnailator.jpg")));
        printImageWidthHeight("scaled imagemagick", readImage(new FileInputStream("gnome-scaled-imagemagick.jpg")));
        printImageWidthHeight("scaled twelvemonkeys", readImage(new FileInputStream("gnome-scaled-twelvemonkeys.jpg")));
    }

    ...
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void printSupportedFormats() {
        String[] readerFormatNames = javax.imageio.ImageIO.getReaderFormatNames();
        String[] writerFormatNames = javax.imageio.ImageIO.getWriterFormatNames();

        System.out.printf("Reader format names: %s%n", String.join(",", readerFormatNames));
        System.out.printf("Writer format names: %s%n", String.join(",", writerFormatNames));
    }

    ...
}
Main-formats.java

En esta lista de formatos aparece webp como formato soportados en la lectura porque en el ejemplo de código está incluida la librería de TwelveMonkeys que añade el soporte para la lectura de webp.

1
2
Reader format names: JPG,jpg,tiff,bmp,BMP,wbp,WBP,gif,GIF,WBMP,png,PNG,JPEG,webp,tif,TIF,TIFF,WEBP,jpeg,wbmp
Writer format names: JPG,jpg,tiff,bmp,BMP,gif,GIF,WBMP,png,PNG,JPEG,tif,TIF,TIFF,wbmp,jpeg
Main-formats.out

Lectura y escritura de una imagen

La clase que representa una imagen en Java es BufferedImage que se obtiene al leer el archivo del sistema de archivos o el InputStream, para la lectura y la escritura está la clase ImageIO.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static BufferedImage readImage(InputStream is) throws IOException {
        return ImageIO.read(is);
    }

    private static void writeImage(BufferedImage image, ImageFormat format, OutputStream os) throws IOException {
        ImageIO.write(image, format.name(), os);
    }

    ...
}
Main-read-write.java

La imagen en formato jpg tiene un tamaño de 192 KB y en formato webp tiene un tamaño de 80 KB, menos de la mitad en webp con una calidad similar.

Imagen en formato jpg Imagen en formato webp

Imagenes en formatos jpg y webp

Obtener el ancho y alto de una imagen

Una vez obtenida una instancia de BufferedImage esta clase ofrece métodos para conocer el ancho y alto de la imagen y a partir de estos la proporción o aspect ratio entre ambos valores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void printImageWidthHeight(String name, BufferedImage image) {
        System.out.printf("Width (%s): %s%n", name, image.getWidth());
        System.out.printf("Height (%s): %s%n", name,  image.getHeight());
    }

    ...
}
Main-width-height.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Width (original): 1920
Height (original): 1072
Width (original webp): 1920
Height (original webp): 1072
Width (scaled): 650
Height (scaled): 362
Width (scaled thumbnailator): 650
Height (scaled thumbnailator): 363
Width (scaled imagemagick): 650
Height (scaled imagemagick): 363
Width (scaled twelvemonkeys): 650
Height (scaled twelvemonkeys): 362
Main-width-height.out

Escalar una imagen

Escalar una imagen consiste en cambiar de tamaño a la imagen, normalmente el escalado se realiza conservando la proporción de anchura y altura para no distorsionar la imagen original.

El escalado suele realizarse para reducir la imagen con lo que se perderá información o píxeles, dependiendo del algoritmo de escalado aplicado la calidad y tamaño de la imagen resultante es diferente. En un escalado que implica reducir el tamaño los bytes de la imagen escalada serán menores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static BufferedImage scaleJava(BufferedImage image) {
        Resolution scaledResolution = new Resolution(image.getWidth(), image.getHeight()).scale(650, 450);
        BufferedImage scaledImage = new BufferedImage(scaledResolution.getWidth(), scaledResolution.getHeight(), BufferedImage.TYPE_INT_RGB);
        Graphics2D graphics2D = scaledImage.createGraphics();
        graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        graphics2D.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        graphics2D.drawImage(image, 0, 0, scaledResolution.getWidth(), scaledResolution.getHeight(), null);
        graphics2D.dispose();
        return scaledImage;
    }

    ...
}
Main-scale-java.java

Dependiendo de las opciones empleadas en el escalado la calidad y tamaño resultante es diferente y hay que conocerlas. Utilizando una librería permite realizar la operación de escalado con mejor calidad.

Imagen escalada con Java

Imagen escalada con Java

Cambiar el formato de una imagen

Cambiar de formato a una imagen es otra operación común, por ejemplo convertir al formato webp que ofrece una calidad mejor en el mismo tamaño o un significativo reducción de tamaño que puede llegar al más del 50% con una calidad similar.

Una vez cargada la imagen en un BufferedImage es posible escribir la imagen a un archivo en cualquier otro formato de escritura soportado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void convertJava(BufferedImage image, ImageFormat format, OutputStream os) throws IOException {
        ImageIO.write(image, format.name().toLowerCase(), os);
    }

    ...
}
Main-convert-java.java

Librerías de terceros

Hay varias librerías que añaden o facilitan algunas funcionalidades sobre la API de Java. Thuilmator permite realizar escalados de imágenes de una forma más sencilla que la API de Java.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void scaleThumbnailator(InputStream is, OutputStream os) throws IOException {
        Thumbnails.of(is)
                .size(650, 450)
                .outputQuality(0.9)
                .outputFormat("jpg")
                .toOutputStream(os);
    }

    ...
}
Main-scale-thuilmator.java

Otra librería es TwelveMonkeys que añade soporte para algunos formatos de imagen como webp.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void scaleTwelvemonkeys(BufferedImage image, OutputStream os) throws IOException, InterruptedException {
        Resolution scaledResolution = new Resolution(image.getWidth(), image.getHeight()).scale(650, 450);
        BufferedImageOp resampler = new ResampleOp(scaledResolution.getWidth(), scaledResolution.getHeight(), ResampleOp.FILTER_LANCZOS);
        BufferedImage scaledImage = resampler.filter(image, null);
        writeImage(scaledImage, ImageFormat.JPG, os);
    }

    ...
}
Main-scale-twelvemonkeys.java

El tamaño de la imagen escalada por el código Java es de 28 KB, el tamaño de la imagen de Thuilmator es de 38 KB y el tamaño de TwelveMonkeys es de 26 KB con algunas pequeñas diferencias apreciables en el resultado de cada una de ellas.

Imagen escalada con Thumbnailator Imagen escalada con Twelvemonkeys Imagen escalada con ImageMagick y Java

Imagen escaladas con diferentes librerías (Thumbnailator, Twelvemonkeys y ImageMagick)

Usando ImageMagick

Las funcionalidades de Java y de las librerías de Java para casos avanzados no son suficientes. El software de ImageMagick ofrece a través de un comando muchas más opciones para la manipulación de imágenes. El inconveniente de ImageMagick es que para ser usado desde Java requiere instalar el comando en el sistema. El uso de ImageMagick desde Java es como la invocación de cualquier otro proceso del sistema, es posible pasar la imagen a manipular por la entrada estándar del sistema y obtener la imagen resultado por la salida estándar del sistema del proceso.

Si no hay ninguna librería Java que proporcione soporte para la escritura a formato webp el siguiente código invoca el comando convert de ImageMagick para realizar la conversión. El tamaño de la imagen en formato webp es de 80 KB que son muchos menos que en cualquiera de los formatos jpg generados con Java de los casos anteriores.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void scaleImageMagick(InputStream is, OutputStream os) throws IOException, InterruptedException {
        ProcessBuilder builder = new ProcessBuilder().command("convert", "-resize", "650x450", "jpeg:-", "jpeg:-");
        Process process = builder.start();

        process.getOutputStream().write(is.readAllBytes());
        process.getOutputStream().close();
        os.write(process.getInputStream().readAllBytes());

        process.waitFor();
        int value = process.exitValue();
        if (value != 0) {
            throw new IOException(MessageFormat.format("Código de salida con error (%d)", value));
        }
    }

    ...
}
Main-scale-imagemagick.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package io.github.picodotdev.blogbitix.javaimageprocess;

...

public class Main {

    ...

    private static void convertImageMagick(InputStream is, OutputStream os) throws IOException, InterruptedException {
        ProcessBuilder builder = new ProcessBuilder().command("convert", "jpeg:-", "webp:-");
        Process process = builder.start();

        process.getOutputStream().write(is.readAllBytes());
        process.getOutputStream().close();
        os.write(process.getInputStream().readAllBytes());

        process.waitFor();
        int value = process.exitValue();
        if (value != 0) {
            throw new IOException(MessageFormat.format("Código de salida con error (%d)", value));
        }
    }
}
Main-convert-imagemagick.java

Imagen convertida a webp con ImageMagick y Java

Imagen convertida a webp con ImageMagick y Java
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run


Comparte el artículo: