/**
*
*/
package ecologylab.bigsemantics.documentparsers;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.awt.image.DirectColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.PackedColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.SinglePixelPackedSampleModel;
import java.awt.image.WritableRaster;
import java.io.BufferedInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.stream.ImageInputStream;
import org.w3c.dom.NodeList;
import com.drew.metadata.exif.ExifDirectory;
import com.drew.metadata.exif.ExifReader;
import com.drew.metadata.exif.GpsDirectory;
import ecologylab.bigsemantics.actions.SemanticsConstants;
import ecologylab.bigsemantics.collecting.SemanticsGlobalScope;
import ecologylab.bigsemantics.downloadcontrollers.DownloadController;
import ecologylab.bigsemantics.metadata.Metadata;
import ecologylab.bigsemantics.metadata.builtins.Image;
import ecologylab.bigsemantics.sensing.GisFeatures;
import ecologylab.bigsemantics.sensing.MetadataExifFeature;
import ecologylab.collections.CollectionTools;
import ecologylab.generic.Debug;
import ecologylab.net.ParsedURL;
/**
* @author andruid
*/
public class ImageParserAwt extends ImageParser
{
private static final String MM_TAG_GPS_LOCATION = "gps_location";
private static final String MM_TAG_CAMERA_SETTINGS = "camera_settings";
private static final int EXIF_TAG_ORIGINAL_DATE = 0x9003;
static final DirectColorModel ARGB_MODEL = new DirectColorModel(32,
0x00ff0000,
0x0000ff00,
0xff,
0xff000000);
static final PackedColorModel RGB_MODEL = new DirectColorModel(24,
0xff0000,
0xff00,
0xff,
0);
static final int[] ARGB_MASKS = { 0xff0000, 0xff00, 0xff, 0xff000000, };
static final int[] RGB_MASKS = { 0xff0000, 0xff00, 0xff, };
static final int[] RGB_BANDS = { 0, 1, 2 };
public static final int MIN_DIM = 10;
public static String EXIF_ELEMENT_TAG_NAME = "unknown";
static final MetadataExifFeature DATE_FEATURE = new MetadataExifFeature(
"creation_date",
ExifDirectory.TAG_DATETIME);
static final MetadataExifFeature ORIG_DATE_FEATURE = new MetadataExifFeature(
"creation_date",
ExifDirectory.TAG_DATETIME_ORIGINAL);
static final MetadataExifFeature CAMERA_MODEL_FEATURE = new MetadataExifFeature(
"model",
ExifDirectory.TAG_MODEL);
// // http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif.html
public static final MetadataExifFeature EXIF_METADATA_FEATURES[] =
{ CAMERA_MODEL_FEATURE,
new MetadataExifFeature("orientation", ExifDirectory.TAG_ORIENTATION),
new MetadataExifFeature("resolution", ExifDirectory.TAG_X_RESOLUTION),
new MetadataExifFeature("exposure_time", ExifDirectory.TAG_EXPOSURE_TIME),
new MetadataExifFeature("aperture", ExifDirectory.TAG_APERTURE),
new MetadataExifFeature("shutter_speed", ExifDirectory.TAG_SHUTTER_SPEED),
new MetadataExifFeature("subject_distance", ExifDirectory.TAG_SUBJECT_DISTANCE), };
static Class cameraClass;
static Class gpsClass;
static final String[] noAlphaMimeStrings = { "image/jpeg", "image/bmp", };
static final HashMap<String, String> noAlphaMimeMap = CollectionTools.buildHashMapFromStrings(noAlphaMimeStrings);
static
{
bindingParserMap.put(SemanticsConstants.IMAGE_PARSER, ImageParserAwt.class);
}
/** Patches a JPEG file that is missing a JFIF marker **/
private static class PatchInputStream extends FilterInputStream
{
private static final int[] JFIF = { 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00,
0x01, 0x02, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00 };
int position = 0;
public PatchInputStream(InputStream in)
{
super(in);
}
@Override
public int read() throws IOException
{
int result;
if (position < 2)
{
result = in.read();
}
else if (position < 2 + JFIF.length)
{
result = JFIF[position - 2];
}
else
{
result = in.read();
}
position++;
return result;
}
@Override
public int read(byte[] b, int off, int len) throws IOException
{
final int max = off + len;
int bytesread = 0;
for (int i = off; i < max; i++)
{
final int bi = read();
if (bi == -1)
{
if (bytesread == 0)
{
bytesread = -1;
}
break;
}
else
{
b[i] = (byte) bi;
bytesread++;
}
}
return bytesread;
}
}
private static ImageInputStream patch(InputStream in) throws IOException
{
in = new BufferedInputStream(in);
in = new PatchInputStream(in);
return ImageIO.createImageInputStream(in);
}
public static String getString(com.drew.metadata.Directory dir, int tag)
{
String result = null;
return result;
}
ImageInputStream imageInputStream;
ImageReader imageReader;
public ImageParserAwt()
{
super();
}
protected void setSemanticsScope(SemanticsGlobalScope semanticsScope)
{
super.setSemanticsScope(semanticsScope);
cameraClass = semanticsScope.getMetadataTypesScope().getClassByTag(MM_TAG_CAMERA_SETTINGS);
gpsClass = semanticsScope.getMetadataTypesScope().getClassByTag(MM_TAG_GPS_LOCATION);
}
@Override
public void parse() throws IOException
{
Image image = getDocument();
BufferedImage bufferedImage = imageIORead(inputStream());
if (bufferedImage != null)
{
image.setParserResult(new ImageParserAwtResult(bufferedImage));
image.setWidth(bufferedImage.getWidth());
image.setHeight(bufferedImage.getHeight());
}
else
Debug.error(image, "ImageParserAwt failed for " + image.getLocation());
}
protected BufferedImage imageIORead(InputStream inputStream) throws IOException
{
BufferedImage bufferedImage = null;
ImageIO.scanForPlugins();
ParsedURL location = getDocument().getLocation();
DownloadController downloadController = getDownloadController();
if (downloadController.accessAndDownload(location))
{
boolean is_ico = false;
// Mime type is checked to determine if the incoming file is a ICO file format
String mimeType = downloadController.getHttpResponse().getMimeType();
if (mimeType == "image/x-icon")
{
is_ico = true;
}
else if ("ico".equals(location.suffix()))
{
is_ico = true;
}
imageInputStream = ImageIO.createImageInputStream(inputStream);
if (imageInputStream == null)
{
error("Cant open ImageInputStream for " + location);
}
else
{
Iterator<ImageReader> imageReadersIterator = ImageIO.getImageReaders(imageInputStream);
if (!imageReadersIterator.hasNext())
error("Cant get reader for " + location);
else
{
while (imageReadersIterator.hasNext())
{
imageReader = imageReadersIterator.next();
if (is_ico == true)
{
// if ICO file, force ICOReader to be used
if ((imageReader.toString().contains("WBMPImageReader")))
{
continue;
}
}
imageReader.setInput(imageInputStream, true, true);
ImageReadParam param = imageReader.getDefaultReadParam();
int width = imageReader.getWidth(0);
int height = imageReader.getHeight(0);
if ((width > MIN_DIM) && (height > MIN_DIM))
{
// try to setup the BufferedImage we use for the read to be structured
// the way we like the data -- as an array of int[].
// this means trying to find out about the image's structure,
// in particular, if its rgb -- 3 color "bands", or argb alread -- 4 bands
int readImageType = -1;
// TODO this line is creating byte arrays :-(
ImageTypeSpecifier rawImageType = imageReader.getRawImageType(0);
// try to find out directly from ImageIO about the file's header
if (rawImageType != null) // unfortunately this doesnt seem to work much for
// URLConnection images
{
int rawNumBands = rawImageType.getNumBands();
switch (rawNumBands)
{
case 4:
readImageType = BufferedImage.TYPE_INT_ARGB;
break;
case 3:
readImageType = BufferedImage.TYPE_INT_RGB;
break;
default:
if (rawImageType.getColorModel() instanceof IndexColorModel)
{
// readImageType= BufferedImage.TYPE_BYTE_INDEXED;
}
break;
}
// debug("gotRawImageType! numBands="+rawNumBands+ " readImageType="+readImageType);
}
// look in the URL itself
else if (location.isNoAlpha()
|| mimeType != null && noAlphaMimeMap.containsKey(mimeType))
{
readImageType = BufferedImage.TYPE_INT_RGB;
}
int[] pixels = null;
DataBufferInt dataBuffer = null;
if (readImageType != -1)
{
ColorModel cm;
int[] masks;
if (readImageType == BufferedImage.TYPE_INT_RGB)
{
cm = RGB_MODEL;
masks = RGB_MASKS;
// param.setDestinationBands(RGB_BANDS);
}
else
{
cm = ARGB_MODEL;
masks = ARGB_MASKS;
}
SampleModel sm = new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT,
width,
height,
masks);
int numPixels = width * height;
pixels = new int[numPixels];
dataBuffer = new DataBufferInt(pixels, numPixels);
WritableRaster wr = Raster.createWritableRaster(sm, dataBuffer, null);
bufferedImage = new BufferedImage(cm, wr, false, null);
/*
* if (graphicsConfiguration != null) { int transparency = (readImageType ==
* BufferedImage.TYPE_INT_ARGB) ? Transparency.TRANSLUCENT : Transparency.OPAQUE;
* bufferedImage = graphicsConfiguration.createCompatibleImage(width, height,
* transparency); } else bufferedImage = new BufferedImage(width, height,
* readImageType);
*/
// Not needed since bufferedImage is filled by the read() call
// param.setDestination(bufferedImage);
}
// read, using the BufferedImage we made, or the default
// if we set result in the line above, we'll just get it back, so no problem.
bufferedImage = imageReader.read(0, param);
if (readImageType == -1)
{
}
if (bufferedImage != null)
{
// throw new IOException("ImageParserAwt Cant read from imageReader for " +
// location);
while (imageReadersIterator.hasNext())
{
ImageReader nextReader = imageReadersIterator.next();
debug("COOL: Freeing resources on additional readers! " + nextReader);
nextReader.reset();
nextReader.dispose();
}
}
}
if (bufferedImage != null)
{
String formatName = imageReader.getFormatName();
if ("JPEG".equals(formatName))
readMetadata(false);
// desparate attempts to reduce referentiality :-)
param.setDestination(null);
param.setSourceBands(null);
param.setDestinationBands(null);
param.setDestinationType(null);
param.setController(null);
break;
}
imageInputStream = ImageIO.createImageInputStream(inputStream);
}
if (bufferedImage == null)
{
throw new IOException("ImageParserAwt Cant read from imageReader for " + location);
}
}
}
}
freeImageIOResources();
return bufferedImage;
}
private void readMetadata(boolean reread) throws IOException
{
try
{
IIOMetadata metadata = imageReader.getImageMetadata(0);
String name = metadata.getNativeMetadataFormatName();
IIOMetadataNode node = (IIOMetadataNode) metadata.getAsTree(name);
// printTree(node);
extractMetadataFeatures(node);
}
catch (javax.imageio.IIOException iioex)
{
// Crazy good workaround for java.imageio bug, from
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4924909
if (!reread && iioex.getMessage() != null
&& iioex.getMessage().endsWith("without prior JFIF!"))
{
warning("Trying workaround for java bug");
closeImageInputStream();
InputStream newInputSteam = reConnect();
imageInputStream = patch(newInputSteam);
imageReader.setInput(imageInputStream);
readMetadata(true);
// IIOImage newImage = imageReader.readAll(0, null);
}
else
warning("Couldn't extract metadata from image: " + iioex);
}
// byte[] iptc =(byte[]) iptcNode.getUserObject();
}
/**
* @param node
* @param exifTag
*/
public void extractMetadataFeatures(IIOMetadataNode node)
{
NodeList unknownElements = node.getElementsByTagName(EXIF_ELEMENT_TAG_NAME);
for (int i = 0; i < unknownElements.getLength(); i++)
{
IIOMetadataNode foundUnknownNode = (IIOMetadataNode) unknownElements.item(i);
if ("225".equals(foundUnknownNode.getAttribute("MarkerTag")))
{
boolean dated = false;
byte[] exifSegment = (byte[]) foundUnknownNode.getUserObject();
final com.drew.metadata.Metadata exifMetadata = new com.drew.metadata.Metadata();
new ExifReader(exifSegment).extract(exifMetadata);
com.drew.metadata.Directory exifDir = exifMetadata.getDirectory(ExifDirectory.class);
Image image = getDocument();
boolean mixedIn = image.containsMixin(MM_TAG_CAMERA_SETTINGS);
if (!mixedIn && !GisFeatures.containsGisMixin(image))
{
if (!dated && ORIG_DATE_FEATURE.extract(image, exifDir) == null)
{
dated = true;
DATE_FEATURE.extract(image, exifDir);
}
if (!mixedIn && CAMERA_MODEL_FEATURE.getStringValue(exifDir) != null)
{
mixedIn = true;
extractMixin(exifDir, EXIF_METADATA_FEATURES, MM_TAG_CAMERA_SETTINGS);
}
com.drew.metadata.Directory gpsDir = exifMetadata.getDirectory(GpsDirectory.class);
Metadata gpsMixin = GisFeatures.extractMixin(gpsDir, getSemanticsScope(), image);
Iterator<com.drew.metadata.Tag> gpsList = printDirectory(gpsDir);
int qq = 33;
}
}
}
}
public void extractMixin(com.drew.metadata.Directory dir, MetadataExifFeature[] features,
String metaMetadataTag)
{
Metadata mixin =
getSemanticsScope().getMetaMetadataRepository().constructByName(metaMetadataTag);
if (mixin != null)
{
extractMetadata(dir, features, mixin);
getDocument().addMixin(mixin);
}
}
public void extractMetadata(com.drew.metadata.Directory dir, MetadataExifFeature[] features,
ecologylab.bigsemantics.metadata.Metadata metadata)
{
for (MetadataExifFeature feature : features)
{
feature.extract(metadata, dir);
}
}
/**
* @param exifDirectory
* @return
*/
public Iterator<com.drew.metadata.Tag> printDirectory(com.drew.metadata.Directory exifDirectory)
{
Iterator<com.drew.metadata.Tag> tagList = exifDirectory.getTagIterator();
while (tagList.hasNext())
{
com.drew.metadata.Tag tag = tagList.next();
System.out.print(tag + " | ");
// if (tag.toString().toLowerCase().contains("gps"))
// System.out.println("EUREKA EURKEA EUREKA! GPS: " + tag + ", ");
}
System.out.println();
return tagList;
}
@Override
public synchronized void recycle()
{
freeImageIOResources();
super.recycle();
}
private boolean freeImageIOResources()
{
boolean result = false;
if (imageReader != null)
{
imageReader.reset(); // release all resources and set to initial state
imageReader.dispose();
imageReader = null;
result = true;
}
result = closeImageInputStream();
return result;
}
private boolean closeImageInputStream()
{
boolean result = false;
if (imageInputStream != null)
{
try
{
imageInputStream.flush();
imageInputStream.close();
imageInputStream = null;
result = true;
}
catch (IOException e)
{
e.printStackTrace();
}
this.imageInputStream = null;
}
return result;
}
/**
* @return false when the thing could not be stopped and a new thread started.
*/
@Override
public synchronized void handleIoError(Throwable e)
{
if (imageReader != null)
imageReader.abort();
freeImageIOResources();
super.error("Caught I/O Error while downloading image:");
e.printStackTrace();
recycle();
}
}