Bueno, lo primero es lo primero. Si a alguien no le suena este término, un thumbnail podemos definirlo como “una imagen en miniatura o previsualización (de una fotografía o imagen)”. Muchos sitios web los utilizan para evitar un retardo en la carga de imágenes demasiado grandes.
Es sencillo generar thumbnails usando programas de retoque fotográfico como Photoshop que controlan muchos aspectos de imágenes, lienzos, … pero si queremos integrarlos automáticamente en nuestros desarrollos necesitaremos algún mecanismo que lo haga por nosotros.
Como un compañero me pidió a ver si podía echarle un cable con este tema, me decidí a implementar un sistema de gestión de thumbnails en Java. Mi idea era lograr que las imagenes generadas pudiesen ser salvadas en todos los formatos de imágenes más usados (a citar GIF, JPG, BMP y PNG) y a la par mantuviera una interfaz de entrada estandar Java, uséase clases File, BufferedImage, … de ahi que descartase el utilizar alguna librería que encontré googleando.
Ciertamente estuve a punto de conseguirlo salvo por el pequeño detalle de que el formato GIF y la clase ImageIO no se llevan especialmente bien (temas de licencias parece que resueltos de modo nativo en Java6), pero el resultado es bastante satisfactorio, ya que salvo eso permite utilizar como netrada ficheros de tipo jpg, gif, bmp y png, pudiendo salvar thumbnails de cualquier formato salvo gif claro :) . De todos modos, lo he dejado codificado de tal modo que cuando se resuelva ese tema, sobre la teoría funcionará para todos los formatos de entrada y salida.
Algunos problemas con los que me encontré en el proceso son:
- Los JPG no admiten transparencia mientras que los GIF y los PNG si. Este fue un problema rico rico que consegui salvar utilizando un BufferedImage de puente para transformar modelos de color ARGB a RGB incluyendo la transparencia. Si intento una conversión con pérdida (PNG -> JPEG), realizo un doble buffering con una primera capa opaca y blanca :p.
- Quizás alguno se pregunte por qué no usé el método getScaledInstance() de Image. Dejo un link para más información. En su lugar, opté por la solución más compleja pero más eficiente que es la de usar transformaciones afines.
- Uno de los mayores cristos con los que me enfrenté fue con el suavizado de imágenes que sufrían algunas transformaciones que superaban el factor de escala 0.5 (umbral parametrizable), en cristiano, aquellas que queremos reducirlas por ejemplo más de un 50%. Sobre todo para el formato JPG (que curiosamente era el que mayores ratios de compresión obtenía y el que más me interesaba) las imágenes en ocasiones se pixelizaban. La solución fue aplicar otra transformación geométrica previa (me gustaría citar el blog del que encontré la solución pero no lo recuerdo :( ), en este caso de convolución para hacer que algunos colores de los pixels se “fusionaran” con otros cercanos con colores semejantes. Con esto se consigue aproximadamente un thuimbnail suavizado en JPG listo para un proyecto web de entre 3 y 4 kb para 150 * 150 pixeles, que creo que es bastante óptimo incluso para aplicaciones de elevado tráfico. A partir de ahi todo depende del tamaño de la imagen que quieras crear y del formato elegido.
- La clase en principio está alimentada con unas opciones de renderización por defecto que tienden a la calidad, pero es posible incluirle un map al momento de construirla con las opciones de renderizado que prefiramos.
Y aqui os dejo el código (quito imports por claridad):
ProcesadorImagenes : Clase principal de gestión de thumbnails. Contiene métodos que permiten crear los thumbnails del tamaño que queramos dando alto y ancho máximo y un objeto de tipo File o BufferedImage.
/** Clase que podemos usar para procesar imagenes en el lenguaje Java. */
public class ProcesadorImagenes {
/* Constantes */
/** Umbral a partir del cual aplicaremos filtros de convolucion . */
protected static final double UMBRAL_APLICACION_FILTRO_CONVOLUCION = 0.5;
/** Factor de convolucion que aplicamos para los algoritmos de
* suavizado. */
private static final Double FACTOR_CONVOLUCION_SUAVIZADO = 1.2;
/* Atributos */
/** Opciones de renderizado para las imagenes. */
protected RenderingHints opcionesRenderizadoImagenes;
/** Listado de formatos a los que debe aplicarse convolucion por
* sus perdidas en caso de reducciones muy pronunciadas. */
protected static List<String> listadoFormatosFiltroReduccionRuido;
/* Metodos */
/** Constructor de la clase. Carga opciones de renderizado
* por defecto, tendiendo a la calidad. */
public ProcesadorImagenes() {
cargarOpcionesRenderizadoDefecto();
cargarListadoFormatosReduccionRuido();
}
/** Constructor de la clase.
* @param opcionesRenderizadoPropias Opciones propias de renderizado
*/
public ProcesadorImagenes(Map<Key,Object> opcionesRenderizadoPropias) {
opcionesRenderizadoImagenes = new RenderingHints(opcionesRenderizadoPropias);
cargarListadoFormatosReduccionRuido();
}
/** Metodo que carga el listado de formatos que requieren de reducccion de
* ruido en caso de reducciones sensibles.*/
private void cargarListadoFormatosReduccionRuido() {
// Lo primero es crear el objeto
listadoFormatosFiltroReduccionRuido = new ArrayList<String>();
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_JPEG_MAYUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_JPEG_MINUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_JPG_MAYUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_JPG_MINUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_BMP_MINUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_BMP_MAYUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_WBMP_MINUSC);
listadoFormatosFiltroReduccionRuido.add(CodigosFormatosImagenes.
CODIGO_FORMATO_WBMP_MAYUSC);
}
/** Metodo que analiza si un formato dado como parametro requiere la aplicacion
* de algoritmos de reduccion de ruido si es una reduccion sensible.
* @param formato Formato sobre el que analizamos
* @return Codigo booleano indicando si se da esta condicion
*/
protected Boolean esFormatoRequiereReduccionRuido(final String formato) {
// Busco en mi listado
Iterator<String> it = listadoFormatosFiltroReduccionRuido.iterator();
while (it.hasNext()) {
// Recupero el formato del interior del iterador
String formatoIt = it.next();
// Comparo ambos
if (formatoIt.equals(formato)) {
// Formato esta en mi lista “negra” :)
return true;
}
}
// Si llego hasta aqui es porque el elemento no estaba
return false;
}
/** Metodo que genera una serie de valores por defecto para las opciones
* de renderizado de las imagenes. */
private void cargarOpcionesRenderizadoDefecto() {
/* Cargo las opciones de renderizado por defecto para la clase: en general,
en este caso se tiende a la calidad de las imagenes generadas */
opcionesRenderizadoImagenes = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_FRACTIONALMETRICS,
RenderingHints.VALUE_FRACTIONALMETRICS_DEFAULT);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BICUBIC);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_STROKE_CONTROL,
RenderingHints.VALUE_STROKE_PURE);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
opcionesRenderizadoImagenes.put(RenderingHints.KEY_COLOR_RENDERING,
RenderingHints.VALUE_COLOR_RENDER_QUALITY);
}
/** Devuelve la lista de formatos disponibles a leer por ImageIO
* @return un array de strings con los mismos.
*/
public String[] dameListadoFormatosUsables(){
return ImageIO.getReaderFormatNames();
}
/** Calcula el factor de escala minimo y en base a eso
* escala la imagen segun dicho factor.
* @param maximoAncho maximo tamaño para el ancho de la nueva imagen
* @param maximoAlto maximo tamaño para el alto de la nueva imagen
* @param formato Formato de la imagen. Determina los filtros a aplicar
* @param imagen Imagen original que vamos a escalar
* @return Devuelve la imagen escalada
* @throws IOException Excepciones de entrada / salida
*/
public BufferedImage escalarATamanyo(final File ficheroImagen,
final Integer maximoAncho, final Integer maximoAlto,
final String formato) throws IOException {
// Lo primero es obtener un BufferedImage
BufferedImage imagenFichero = ImageIO.read(ficheroImagen);
// Aplico otro metodo de la clase
return escalarATamanyo(imagenFichero, maximoAncho, maximoAlto, formato);
}
/** Calcula el factor de escala minimo y en base a eso
* escala la imagen segun dicho factor.
* @param maximoAncho maximo tamaño para el ancho de la nueva imagen
* @param maximoAlto maximo tamaño para el alto de la nueva imagen
* @param formato Formato de la imagen. Determina los filtros a aplicar
* @param imagen Imagen original que vamos a escalar
* @return Devuelve la imagen escalada
*/
public BufferedImage escalarATamanyo(final BufferedImage imagen,
final Integer maximoAncho, final Integer maximoAlto,
final String formato) {
// Comprobacion de parametros
if (imagen == null || maximoAlto <= 0 || maximoAncho <= 0) {
return imagen;
}
// Capturo ancho y alto de la imagen
int anchoImagen = imagen.getHeight();
int altoImagen = imagen.getWidth();
// Segunda comprobacion de parametros
if (anchoImagen == 0 || altoImagen == 0) {
return imagen;
}
// Calculo la relacion entre anchos y altos de la imagen
double escalaX = (double)maximoAncho / (double)anchoImagen;
double escalaY = (double)maximoAlto / (double)altoImagen;
// Tomo como referencia el minimo de las escalas
double fEscala = Math.min(escalaX, escalaY);
// Devuelvo el resultado de aplicar esa escala a la imagen
return escalar(fEscala, imagen, formato);
}
/** Escala una imagen en porcentaje.
* @param factorEscala ejemplo: factorEscala=0.6 (escala la imagen al 60%)
* @param srcImg una imagen en formato BufferedImage
* @param formatoOrigen Formato de la imagen. Determina los filtros a aplicar
* @return un BufferedImage escalado
*/
public BufferedImage escalar(final Double factorEscala,
final BufferedImage srcImg, final String formatoOrigen) {
// Comprobacion de parametros
if (srcImg == null || factorEscala == 0) {
return null;
}
// Preparo el tipo de los nuevos BufferedImage
int tipoFormatoBufferedReader;
if (formatoOrigen.equals(CodigosFormatosImagenes.CODIGO_FORMATO_GIF)) {
tipoFormatoBufferedReader = srcImg.getType();
} else {
tipoFormatoBufferedReader = BufferedImage.TYPE_INT_RGB;
}
// Caso de que realmente tengamos que escalar …
BufferedImage filtroInicial = null;
// Compruebo escala nula
if (factorEscala == 1) {
// En ese caso, devuelvo una copia de la imagen original
BufferedImage copia = new BufferedImage (srcImg.getWidth(),
srcImg.getHeight(), tipoFormatoBufferedReader);
copia.setData(srcImg.getData());
return copia;
} else {
// Se trata de una reduccion muy acuciada ?
if (factorEscala < UMBRAL_APLICACION_FILTRO_CONVOLUCION &&
esFormatoRequiereReduccionRuido(formatoOrigen)) {
/* Para las imagenes cuyo factor de escala sea menor que el 0.5 …
Preparo un objeto de tipo Kernel */
Kernel kernel = crearKernelEscala(factorEscala);
// Lanzo una transformacion afin previa de suavizado
ConvolveOp op = new ConvolveOp(
kernel, ConvolveOp.EDGE_NO_OP, null);
// Almaceno en filtroInicial la imagen suavizada
BufferedImage copia = new BufferedImage (srcImg.getWidth(),
srcImg.getHeight(), tipoFormatoBufferedReader);
copia.setData(srcImg.getData());
filtroInicial = op.filter(copia, filtroInicial);
}
else {
// Factores de escala sin suavizado
filtroInicial = srcImg;
}
}
// De aqui en adelante, debemos trabajar en base a filtroInicial
// La creo con las opciones de renderizado que tuviesemos
AffineTransformOp op = new AffineTransformOp(AffineTransform.getScaleInstance(factorEscala, factorEscala),
opcionesRenderizadoImagenes);
BufferedImage resultadoFiltro = op.filter(filtroInicial, null);
/* Balanceo entre elementos BufferedImage para eliminar canales
de transparencia extras, si hay */
BufferedImage biConversion = new BufferedImage (resultadoFiltro.getWidth(),
resultadoFiltro.getHeight(), tipoFormatoBufferedReader);
Graphics2D g = biConversion.createGraphics();
g.setRenderingHints(opcionesRenderizadoImagenes);
g.drawImage(resultadoFiltro, 0, 0, Color.WHITE , null);
// Devuelvo el resultado de aplicar el filtro sobre la imagen
return biConversion;
}
/** Metodo que guarda una imagen en disco
* @param imagen Imagen a almacenar en disco
* @param rutaFichero Ruta de la imagen donde vamos a salvar la imagen
* @param formato Formato de la imagen al almacenarla en disco
* @return Booleano indicando si se consiguio salvar con exito la imagen
* @throws IOException Excepciones de entrada / salida generales
*/
public Boolean salvarImagen(final BufferedImage imagen,
final String rutaFichero, final String formato)
throws IOException {
// Comprobacion de parametros
if (imagen != null && rutaFichero != null && formato != null) {
ImageIO.write( imagen, formato, new File( rutaFichero ));
return true;
} else {
// Fallo en tema de parametros
return false;
}
}
/** Metodo que crea un objeto de tipo Kernel para aplicar en
* posteriores transformaciones.
* @param factorEscala Factor de escala que tiene la imagen
* @return Objeto Kernel construido
*/
private Kernel crearKernelEscala(final Double factorEscala) {
// Calculos matematicos de proporciones de suavizado
int tamanyo = 1 + (int) (FACTOR_CONVOLUCION_SUAVIZADO / factorEscala);
float[] datos = new float[tamanyo * tamanyo];
float factor = 1 / (float) datos.length;
for (int i = 0; i < datos.length; i++) {
datos[i] = factor;
}
// Devuelvo un objeto Kernel entendible por el API
return new Kernel(tamanyo, tamanyo, datos);
}
}
CodigosFormatosImagenes: Por claridad, separé los códigos de los formatos de las imágenes en otra clase independiente.
/** Clase de constantes que nos ayuda a mantener algunos codigos de
* formatos graficos utiles para las comparaciones. */
public class CodigosFormatosImagenes {
/* Constantes */
/** Codigo para el formato GIF – Graphics Interchange Format . */
public static final String CODIGO_FORMATO_GIF = “gif”;
/** Codigo para el formato PNG – Portable Network Graphics . */
public static final String CODIGO_FORMATO_PNG = “png”;
/** Codigo para el formato BMP en minusculas – Mapa de Bits . */
public static final String CODIGO_FORMATO_BMP_MINUSC = “bmp”;
/** Codigo para el formato BMP en mayusculas – Mapa de Bits . */
public static final String CODIGO_FORMATO_BMP_MAYUSC = “BMP”;
/** Codigo para el formato JPG en minusculas -
* Joint Photographic Experts Group . */
public static final String CODIGO_FORMATO_JPG_MINUSC = “jpg”;
/** Codigo para el formato JPG en mayusculas -
* Joint Photographic Experts Group . */
public static final String CODIGO_FORMATO_JPG_MAYUSC = “JPG”;
/** Codigo para el formato JPEG en minusculas -
* Joint Photographic Experts Group . */
public static final String CODIGO_FORMATO_JPEG_MINUSC = “jpeg”;
/** Codigo para el formato JPEG en mayusculas -
* Joint Photographic Experts Group . */
public static final String CODIGO_FORMATO_JPEG_MAYUSC = “JPEG”;
/** Codigo para el formato WBMP en mayusculas -
* Wireless Application Protocol Bitmap Format . */
public static final String CODIGO_FORMATO_WBMP_MINUSC = “wbmp”;
/** Codigo para el formato WBMP en mayusculas -
* Wireless Application Protocol Bitmap Format. */
public static final String CODIGO_FORMATO_WBMP_MAYUSC = “WBMP”;
}