Cominciamo con questo numero una serie tecnica che, in maniera molto sintetica, si occuperà di presentare delle API Java in grado di interagire con applicazioni desktop e/o mobile. La disponibilità di API, specie per certe funzioni e per certe applicazioni è infatti un elemento cruciale per il lavoro degli sviluppatori. Cominciamo questa carrellata con Apache Commons Imaging.
Introduzione
Apache Commons imaging è una libreria che risultare molto utile nella manipolazione delle immagini, in operazioni quali conversioni, scale di colore, lettura e inserimento di metadati all’interno di esse. Ancora una volta la comunità Apache ci aiuta fornendoci questa preziosa libreria, precedentemente conosciuta come Apache Commons Sanselan.
Si tratta di una libreria Java che permette di leggere e scrivere diversi formati di immagini e lavora su tutte le loro proprietà (dimensioni, colori, profili, e così via). Questa libreria è al 100% puro codice Java: ciò implica che sia più lenta, ma anche la sua perfetta portabilità.
Rispetto a ImageIO / JAI / toolkit (supportate da Java), Apache Commons Imaging è più facile da usare, e inoltre supporta più formati rispetto a quelli standard, consentendo poi di accedere ai metadati in maniera più semplice e diretta. I requisiti e le caratteristiche sono illustrate di seguito:
- Java 1.5+
- zero dipendenza da altri JAR
- non usa ImageIO / AWT
Supporto per formati immagine
Vediamo anzitutto le capacità di Apache Commons Imaging per il supporto dei diversi formati immagine, riportando l’estensione, le capacità di lettura e scrittura dello stesso e una serie di note esplicative
BMP
Supporto in lettura e scrittura. Quasi completo, ma potrebbe non leggere qualche cursore, qualche icona e qualche bitmpa OS/2. Non è ancora completo il controllo del formato esatto quando si scrive.
GIF
Supporto in lettura e scrittura per entrambe le versioni 87a e 89a. La lettura delle GIF animate è supportata fino al punto di poter leggere tutte le immagini contenute in una GIF, ma la temporizzazione e la visualizzazione ciclica sono ignorate. Non è ancora completo il controllo del formato esatto quando si scrive.
JPEG / JFIF
Parziale supporto in lettura, mancanza di supporto in scrittura. Legge solo immagini JPEG semplici in scala di grici o YCbCr a linea di base sequenziale, senza marcatori RST, che devono usare 8 bit per componente ed essere compresse secondo la codidica di Huffmann. Può leggere le informazioni e i metadati delle immagini ed estrarre i profili ICC, sia dalle JFIF che da DCF/EXIF. Fornisce commenti JPEG in ImageInfo.
ICNS
Supporto in massima parte sia in lettura che in scrittura. Manca il supporto per le icone JPEG2000, ma tutti gli altri formati e le altre misure sono lette e scritte correttamente. Testato estensivamente per la correttezza in Mac OS X, compreso il comportamento in caso di mancanza di maschera.
!CO / CUR
Supporto in lettura e, in massima parte, anche in scrittura. Legge fil .ico e .cur a 1 / 4 / 8 / 16 / 24 / 32 bpp. Supporta i file ICO di Windows con i PNG incorporati. Gestisce correttamente i problemi di trasparenza del canale alfa vs. btmask a profondità di colore di 32 bit per pixel. Supporta i bitmap co compressione bitfield. Testato estensivamente, è con ogni probabilità la migliore implementazione open source disponibile di questo supporto.
PCX / DCX
Supporto in lettura e scrittura. Legge immagini a 1 piano e 1 / 2 / 4 / 8 bit, 1 bit e 1 / 2 / 3 / 4 piani, 3 piani a 8 bit, 1 piano a 24 bit e 1 piano a 32 bit. Le immagini monocrome vengono correttamente interpretate. Legge e può scrivere il formato non compresso e non documentato PCX, compresa la lettura delle tabelle DCX ridotte. Testato completamente.
PNM / PGM / PBM / PPM / PAM Portable Pixmap
Supporto completo in lettura e, in massima parte, anche in scrittura. Scrittura del PAM solo nel formato RGB_ALPHA.
PNG
Supporto in lettura e scrittura. Supportato fino a tutta la versione 1.2 in standard ISO/IEC (15948:2003). Non è ancora completo il controllo del formato esatto quando si scrive.
PSD / Photoshop
Supporto in lettura ma non in scrittura. Supporto delle funzioni base: può leggere solo il primo layer, senza supporto per i canali extra. Supporto per tutte le modalità eccetto che per la multicanale. Può keggere alcuni metadati dell’immagine.
RGBE / Radiance HDR
Supporto in lettura ma non in scrittura. Supporto delle funzioni base.
TIFF
Supporto in lettura e scrittura. Supportato fino a tutta la versione 6.0. Il TIFF è un formato “contenitore” aperto, quindi non è possibile supportare ogni sua possibile variante. Sono supportate le immagini bi-level, palette / indexed, RGB, CMYK, YCbCr, CIELab e LOGLUV. Supporta la lettura e la scrittura degli algoritmi di compressione CCITT / modified Huffman / Group 3 / Group 4 e Packbits / RLE compression. Mancano però altre forme di compressione, segnatamente la JPEG. Supporta la lettura di immagini sezionate.
WBMP
Supporto in lettura e scrittura. Supporto completo per i bitmap WBMP tipo 0.
XBM
Supporto in lettura e scrittura completo.
XPM
Supporto in lettura e scrittura. Attualmente è supportata solo la versione 3 di XPM, ma le altre versioni sono obsolete. Legge tutti i formati di colore, compresi quelli che utilizzano nomi simbolici provenienti da rgb.txt. La scrittura scrve solo i dati sul colore.
Supporto per formati metadati
Apache Commons Imaging supporta i seguenti formati di metadati.
Metadati EXIF per JPEG/JFIF
Supporto in lettura e scrittura. Può leggere e scrivere dati EXIF da e verso file JPEG/JFIF esistenti senza modificare i dati immagine.
Metadati IPTC per JPEG/JFIF
Supporto in lettura già presente, supporto in scrittura ancora da implementare, ma ormai prossimo. Può leggere dati IPTC da file JPEG/JFIF esistenti senza modificare i dati immagine.
XMP
Supporto in lettura e scrittura. Riesce a leggere XMP XML (come String) da TIFF, GIF, PNG, JPEG e PSD. Può incorporare XMP XML in scrittura su GIF, PNG e TIFF. Può rimuovere, inserire e aggiornare XMP XML dentro file JPEG esistenti.
Preparazione dell’ambiente di lavoro
Ora che abbiamo visto le caratteristiche di supporto a formati immagine e formati di metadati, cerchiamo di capire come fare per installare Apache Commons Imaging. Per scaricare questa libreria andiamo sul sito internet della comunità Apache
http://commons.apache.org/proper/commons-imaging/index.html
dove troveremo tutte le informazioni sulla nostra libreria. Spostiamoci sul menu Imaging -> Download e scarichiamo i file binari
apache-sanselan-incubating-0.97-bin.zip
Figura 1 – La pagina del progetto, da cui scaricare i file della libreria.
Una volta scaricata la libreria, includiamola nel nostro progetto e saremo pronti per iniziare a scrivere un po’ di codice.
Lettura di un’immagine
La lettura di un’immagine attraverso questa libreria avviene con il metodo getBufferedImage() che vuole come parametri d’ingresso il file dell’immagine e i parametri relativi all’immagine.
Imaging.getBufferedImage(file, params);
Le informazioni sui parametri possono essere consultate all’interno della documentazione ufficiale [1]. Di seguito mostriamo una breve lista di parametri che si possono utilizzare (tabella 1).
Tabella 1 – Elenco di parametri da utilizzare per la lettura di una immagine.
Scrittura di un’immagine
Per scrivere un immagine su un file, invece, viene usata la funzione writeImageToBytes()
Imaging.writeImageToBytes(image, format, params);
che richiede come parametri di ingresso l’immagine, il tipo di formato e parametri opzionali sull’immagine. Ecco un esempio:
public class ScritturaImgEsempio { public static byte[] imageWriteExample(final File file) throws ImageReadException, ImageWriteException, IOException { // lettura final BufferedImage image = Imaging.getBufferedImage(file); final ImageFormat format = ImageFormats.TIFF; final Map<String, Object> params = new HashMap<String, Object>(); // settaggio di parametri opzionali params.put(ImagingConstants.PARAM_KEY_COMPRESSION, new Integer( TiffConstants.TIFF_COMPRESSION_UNCOMPRESSED)); final byte[] bytes = Imaging.writeImageToBytes(image, format, params); return bytes; } }
Profili ICC
I profili ICC (International Color Consortium) descrivono gli attributi di colore di un particolare dispositivo. Per una definizione più dettagliata, vi rimandiamo voce “Profilo ICC” [2]. Ora andiamo a vedere come questi profili vengono usati in Java attraverso la libreria di Apache.
final byte iccProfileBytes[] = Imaging.getICCProfileBytes(imageBytes); final ICC_Profile iccProfile = Imaging.getICCProfile(imageBytes);
Metadati
I metadati contenuti nelle immagini sono molto usati al giorno d’oggi e vengono salvati nelle foto scattate da uno smarthpone o da un tablet ma anche in quelle realizzate con fotocamere reflex digitali.
Infatti insieme alle foto possono essere salvate informazioni riguardanti la data di creazione, l’orientamento della foto, la distanza focale, il tempo di esposizione, lo spazio colore, le coordinate GPS, e molte altre. Questi dati sono molto importanti sia, ad esempio, per georiferire le fotografie e collocarle su una mappa, sia per poter effettuare elaborazioni di tipo fotografico con gli appositi programmi di fotoritocco. Non vanno poi sottovalutate le attenzioni da riservare alla sicurezza di tali informazioni.
Vedremo ora di seguito il codice che mostra un esempio pratico di lettura e di scrittura di metadati in un’immagine: i brevi commenti riportati nel codice specificano alcuni aspetti cui raccomandiamo di prestare attenzione, ma il codice non dovrebbe risultare di difficile interpretazione.
Lettura di metadati
public class MetadataExample { public static void metadataExample(final File file) throws ImageReadException, IOException { // ottengo metadata nel formato EXIF (es. JPEG o TIFF) final IImageMetadata metadata = Imaging.getMetadata(file); if (metadata instanceof JpegImageMetadata) { final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; System.out.println("file: " + file.getPath()); // print out various interesting EXIF tags. printTagValue(jpegMetadata, TiffTagConstants.TIFF_TAG_XRESOLUTION); printTagValue(jpegMetadata, TiffTagConstants.TIFF_TAG_DATE_TIME); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_DATE_TIME_DIGITIZED); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_ISO); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_SHUTTER_SPEED_VALUE); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_APERTURE_VALUE); printTagValue(jpegMetadata, ExifTagConstants.EXIF_TAG_BRIGHTNESS_VALUE); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LATITUDE); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF); printTagValue(jpegMetadata, GpsTagConstants.GPS_TAG_GPS_LONGITUDE); System.out.println(); // Semplice interfaccia GPS final TiffImageMetadata exifMetadata = jpegMetadata.getExif(); if (null != exifMetadata) { final TiffImageMetadata.GPSInfo gpsInfo = exifMetadata.getGPS(); if (null != gpsInfo) { final String gpsDescription = gpsInfo.toString(); final double longitude = gpsInfo.getLongitudeAsDegreesEast(); final double latitude = gpsInfo.getLatitudeAsDegreesNorth(); System.out.println(" " + "GPS Description: " + gpsDescription); System.out.println(" " + "GPS Longitude (Degrees East): " + longitude); System.out.println(" " + "GPS Latitude (Degrees North): " + latitude); } } // Esempi specifici di come accedere alle coordinate GPS final TiffField gpsLatitudeRefField = jpegMetadata.findEXIFValueWithExactMatch( GpsTagConstants.GPS_TAG_GPS_LATITUDE_REF); final TiffField gpsLatitudeField = jpegMetadata.findEXIFValueWithExactMatch( GpsTagConstants.GPS_TAG_GPS_LATITUDE); final TiffField gpsLongitudeRefField = jpegMetadata.findEXIFValueWithExactMatch( GpsTagConstants.GPS_TAG_GPS_LONGITUDE_REF); final TiffField gpsLongitudeField = jpegMetadata.findEXIFValueWithExactMatch( GpsTagConstants.GPS_TAG_GPS_LONGITUDE); if (gpsLatitudeRefField != null && gpsLatitudeField != null && gpsLongitudeRefField != null && gpsLongitudeField != null) { // all of these values are strings. final String gpsLatitudeRef = (String) gpsLatitudeRefField.getValue(); final RationalNumber gpsLatitude[] = (RationalNumber[]) (gpsLatitudeField .getValue()); final String gpsLongitudeRef = (String) gpsLongitudeRefField.getValue(); final RationalNumber gpsLongitude[] = (RationalNumber[]) gpsLongitudeField.getValue(); final RationalNumber gpsLatitudeDegrees = gpsLatitude[0]; final RationalNumber gpsLatitudeMinutes = gpsLatitude[1]; final RationalNumber gpsLatitudeSeconds = gpsLatitude[2]; final RationalNumber gpsLongitudeDegrees = gpsLongitude[0]; final RationalNumber gpsLongitudeMinutes = gpsLongitude[1]; final RationalNumber gpsLongitudeSeconds = gpsLongitude[2]; // Stampa del formato GPS System.out.println(" " + "GPS Latitude: " + gpsLatitudeDegrees.toDisplayString() + " degrees, " + gpsLatitudeMinutes.toDisplayString() + " minutes, " + gpsLatitudeSeconds.toDisplayString() + " seconds " + gpsLatitudeRef); System.out.println(" " + "GPS Longitude: " + gpsLongitudeDegrees.toDisplayString() + " degrees, " + gpsLongitudeMinutes.toDisplayString() + " minutes, " + gpsLongitudeSeconds.toDisplayString() + " seconds " + gpsLongitudeRef); } System.out.println(); final List items = jpegMetadata.getItems(); for (int i = 0; i < items.size(); i++) { final IImageMetadataItem item = items.get(i); System.out.println(" " + "item: " + item); } System.out.println(); } } // Stampa valori metadata private static void printTagValue(final JpegImageMetadata jpegMetadata, final TagInfo tagInfo) { final TiffField field = jpegMetadata.findEXIFValueWithExactMatch(tagInfo); if (field == null) { System.out.println(tagInfo.name + ": " + "Not Found."); } else { System.out.println(tagInfo.name + ": " + field.getValueDescription()); } } }
Scrittura metadati
Vediamo ora come scrivere i metadati su una immagine grazie alle funzionalità della libreria Apache Commons Imaging. Anche in questo caso, raccomandiamo particolare attenzione ai commenti nel codice, che ne illustrano alcuni aspetti importanti.
public class WriteExifMetadataExample { public void removeExifMetadata(final File jpegImageFile, final File dst) throws IOException, ImageReadException, ImageWriteException { OutputStream os = null; boolean canThrow = false; try { os = new FileOutputStream(dst); os = new BufferedOutputStream(os); new ExifRewriter().removeExifMetadata(jpegImageFile, os); canThrow = true; } finally { IoUtils.closeQuietly(canThrow, os); } } // Questo esempio spiega come aggiungere o aggiornare dati EXIF in un JPEG public void changeExifMetadata(final File jpegImageFile, final File dst) throws IOException, ImageReadException, ImageWriteException { OutputStream os = null; boolean canThrow = false; try { TiffOutputSet outputSet = null; // se non viene trovato alcun metadato, // i metadati potrebbero essere null final IImageMetadata metadata = Imaging.getMetadata(jpegImageFile); final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; if (null != jpegMetadata) { // notare che gli EXIF potrebbero essere null //se non viene trovato alcun EXIF final TiffImageMetadata exif = jpegMetadata.getExif(); if (null != exif) { // Classe TiffImageMetadata è immutabile (sola lettura). // Classe TiffOutputSet rappresenta i dati EXIF da scrivere. // Di solito, vogliamo aggiornare i metadati EXIF esistenti cambiando // i valori di alcuni campi o aggiungendo un campo. // In questi casi, è più facile usare getOutputSet () per // copiare dei campi letti dall'immagine. outputSet = exif.getOutputSet(); } } // Se il file non contiene metadati EXIF, creiamo un vuoto if (null == outputSet) { outputSet = new TiffOutputSet(); } { // Esempio di come aggiungere un campo / tag all'output // si veda org.apache.commons.imaging.formats // .tiff.constants.AllTagConstants final TiffOutputDirectory exifDirectory = outputSet .getOrCreateExifDirectory(); // Assicurarsi di rimuovere il vecchio valore se presente // altrimenti andrà in eccezione perchè il tag non esiste exifDirectory .removeField(ExifTagConstants.EXIF_TAG_APERTURE_VALUE); exifDirectory.add(ExifTagConstants.EXIF_TAG_APERTURE_VALUE, new RationalNumber(3, 10)); } { //Esempio di come aggiungere / aggiornare informazioni GPS // New York City final double longitude = -74.0; final double latitude = 40 + 43 / 60.0; outputSet.setGPSInDegrees(longitude, latitude); } os = new FileOutputStream(dst); os = new BufferedOutputStream(os); new ExifRewriter().updateExifMetadataLossless(jpegImageFile, os, outputSet); canThrow = true; } finally { IoUtils.closeQuietly(canThrow, os); } } // Questo esempio illustra come rimuovere un tag (se presente) da EXIF public void removeExifTag(final File jpegImageFile, final File dst) throws IOException,ImageReadException, ImageWriteException { OutputStream os = null; boolean canThrow = false; try { TiffOutputSet outputSet = null; // notare che i metadati potrebbero essere null // se non viene trovato alcun metadato final IImageMetadata metadata = Imaging.getMetadata(jpegImageFile); final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; if (null != jpegMetadata) { // notare che gli EXIF potrebbero essere null //se non viene trovato alcun EXIF final TiffImageMetadata exif = jpegMetadata.getExif(); if (null != exif) { // Classe TiffImageMetadata è immutabile (sola lettura) . // Classe TiffOutputSet rappresenta i dati EXIF da scrivere . // Di solito, vogliamo aggiornare i metadati EXIF esistenti // cambiando i valori di alcuni campi o aggiungere un campo. // In questi casi, è più facile usare getOutputSet () per // copiare dei campi letti dall'immagine. outputSet = exif.getOutputSet(); } } if (null == outputSet) { // Il file non contiene tutti i metadati EXIF. // Non abbiamo bisogno di aggiornare il file; basta copiarlo. FileUtils.copyFile(jpegImageFile, dst); return; } { // Esempio di come rimuovere un singolo tag / campo. final TiffOutputDirectory exifDirectory = outputSet .getExifDirectory(); if (null != exifDirectory) { exifDirectory .removeField(ExifTagConstants.EXIF_TAG_APERTURE_VALUE); } } os = new FileOutputStream(dst); os = new BufferedOutputStream(os); new ExifRewriter().updateExifMetadataLossless(jpegImageFile, os, outputSet); canThrow = true; } finally { IoUtils.closeQuietly(canThrow, os); } } // Questo esempio mostra come salvare i valori // delle coordinate GPS nel JPEG EXIF public void setExifGPSTag(final File jpegImageFile, final File dst) throws IOException, ImageReadException, ImageWriteException { OutputStream os = null; boolean canThrow = false; try { TiffOutputSet outputSet = null; final IImageMetadata metadata = Imaging.getMetadata(jpegImageFile); final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata; if (null != jpegMetadata) { final TiffImageMetadata exif = jpegMetadata.getExif(); if (null != exif) { outputSet = exif.getOutputSet(); } } if (null == outputSet) { outputSet = new TiffOutputSet(); } { // Esempio di come aggiungere o aggiornare coordinate GPS outputSet.setGPSInDegrees(longitude, latitude); } os = new FileOutputStream(dst); os = new BufferedOutputStream(os); new ExifRewriter().updateExifMetadataLossless(jpegImageFile, os, outputSet); canThrow = true; } finally { IoUtils.closeQuietly(canThrow, os); } } }
Un esempio riassuntivo
Di seguito, si riporta un ulteriore esempio riassuntivo del modo in cui può essere utilizzata questa libreria. Ancora una volta, raccomandiamo ai lettori di fare attenzione ai commenti nel codice.
public class SampleUsage { public SampleUsage() { try { //Il codice non funziona a meno che queste variabili // siano correttamente inizializzate //Imaging funziona altrettanto bene con File, // array di byte o InputStream. final BufferedImage someImage = null; final byte someBytes[] = null; final File someFile = null; final InputStream someInputStream = null; final OutputStream someOutputStream = null; // Leggere una immagine final byte imageBytes[] = someBytes; final BufferedImage image_1 = Imaging.getBufferedImage(imageBytes); final BufferedImage image_2 = Imaging.getBufferedImage(imageBytes); final File file = someFile; final BufferedImage image_3 = Imaging.getBufferedImage(file); final InputStream is = someInputStream; final BufferedImage image_4 = Imaging.getBufferedImage(is); // Scrivere un'immagine final BufferedImage image = someImage; final File dst = someFile; final ImageFormat format = ImageFormats.PNG; final Map<String, Object> optionalParams = new HashMap<String, Object>(); Imaging.writeImage(image, dst, format, optionalParams); final OutputStream os = someOutputStream; Imaging.writeImage(image, os, format, optionalParams); // Ottengo il Profilo ICC se esiste final byte iccProfileBytes[] = Imaging.getICCProfileBytes(imageBytes); final ICC_Profile iccProfile = Imaging.getICCProfile(imageBytes); // Altezza e larghezza dell'immagine final Dimension d = Imaging.getImageSize(imageBytes); // Ottengo info sull'immagine come bit per pixel, // dimensioni, trasparenza, etc. final ImageInfo imageInfo = Imaging.getImageInfo(imageBytes); if (imageInfo.getColorType() == ImageInfo.COLOR_TYPE_GRAYSCALE) { System.out.println("Grayscale image."); } if (imageInfo.getHeight() > 1000) { System.out.println("Large image."); } // Prova a desumere il formato dell'immagine final ImageFormat imageFormat = Imaging.guessFormat(imageBytes); imageFormat.equals(ImageFormats.PNG); // Ottiene tutti i metadati in formato EXIF final IImageMetadata metadata = Imaging.getMetadata(imageBytes); // Stampa informazioni su file Imaging.dumpImageFile(imageBytes); /* Ottenere indice degli errori */ final FormatCompliance formatCompliance = Imaging.getFormatCompliance(imageBytes); } catch (final Exception e) { } } }
Conclusioni
Apache Commons Imaging si dimostra una buona libreria, soprattutto pratica per manipolare immagini e metadati che possono essere aggiunti ad esse. Ha ancora margini di miglioramento e speriamo che la grande comunità Apache la aggiorni con una certa costanza.
Nei prossimi articoli affronteremo altre API, tra le quali, solo per citarne alcune, Apache POI per l’interazione con la suite Microsoft Office, PDFbox per lavorare su file PDF e Twitter4J per interfacciarsi alla piattaforma social di microblogging.