DepthMapNeedle/src/JPEGUtils.java

511 lines
15 KiB
Java
Raw Normal View History

import java.util.Arrays;
import java.util.Vector;
public class JPEGUtils extends Const
{
2015-04-13 22:42:16 +02:00
/**
* Read the length of the block from the two bytes after the APP1 Marker
*
2015-04-13 22:42:16 +02:00
* @param boundaryPos position of the APP1 Marker in the byte array
* @param data byte array containing the block WITH APP1 Marker
2015-04-13 22:42:16 +02:00
* @return length of the block
*/
public static int readBlockLength(int boundaryPos, byte[] data)
{
2015-04-13 22:42:16 +02:00
//Check whether entered position is APP1 Marker
if (!JPEGUtils.isAPP1Marker(data, boundaryPos))
{
System.err.println("JPEGUtils.blockLength(): Block is no APP1 Block!");
return -1;
}
int o;
2015-04-13 22:42:16 +02:00
//Read length
try
{
o = 256 * (data[boundaryPos+2] & 0xFF)+(data[boundaryPos+3] & 0xFF);
} catch (ArrayIndexOutOfBoundsException e)
{
System.err.println("JPEGUtils.blockLength() threw ArrayIndexOutOfBoundsException. Maybe the block is cut after APP1 Marker?");
e.printStackTrace();
return -1;
}
return o;
}
2015-04-13 22:42:16 +02:00
/**
* decorate information with a block header of a certain type. The type is specified via the argument type.
* In case, type == EXTENDEDXMP and data.length > CHUNKSIZE multiple ExtendedXMPBlocks will be generated and concatenated.
* Available types:
* Const.EXIF, Const.STANDARDXMP, Const.EXTENDEDXMP
*
2015-04-13 22:42:16 +02:00
* @param data byte array of data that will be decorated
* @param type String declaring the type of header for the block.
* @return decorated block
*/
public static byte[] decorateBlock(byte[] data, String type)
{
2015-04-13 22:42:16 +02:00
//EXIF Block: 'APP1 + BLOCKLENGTH + EXIF\0\0 + data'
if (type.equals(EXIF))
{
data = ArrayUtils.concatenate(markEXIF, data);
byte[] pre = ArrayUtils.concatenate(markAPP1, genLen(data.length+2));
return ArrayUtils.concatenate(pre, data);
}
2015-04-13 22:42:16 +02:00
//StandardXMP: 'APP1 + BLOCKLENGTH + http://ns.adobe.com/xap/1.0/\0 + data'
else if (type.equals(STANDARDXMP))
{
data = ArrayUtils.concatenate(markStandardXMP, data);
byte[] pre = ArrayUtils.concatenate(markAPP1, genLen(data.length+2));
return ArrayUtils.concatenate(pre, data);
}
2015-04-13 22:42:16 +02:00
//ExtendedXMP: 'APP1 + BLOCKLENGTH + http://ns.adobe.com/xmp/extension/\0 + MD5 + EXTENDEDLENGTH + EXTENDEDOFFSET + DATAPORTION
else if (type.equals(EXTENDEDXMP))
{
byte[] out = new byte[0];
2015-04-13 22:42:16 +02:00
//MD5 checksum is digest of the datacontent
byte[] md5 = HexUtil.generateMD5(data);
int i = 0;
int blockCount = data.length / CHUNKSIZE;
2015-04-13 22:42:16 +02:00
//decorate blockportions of size CHUNKSIZE
while (i < blockCount)
{
byte[] part = Arrays.copyOfRange(data, i * CHUNKSIZE, (i+1) * CHUNKSIZE);
byte[] pre = markAPP1;
pre = ArrayUtils.concatenate(pre, genLen(2+markExtendedXMP.length+32+4+4+part.length));
pre = ArrayUtils.concatenate(pre, markExtendedXMP);
pre = ArrayUtils.concatenate(pre, md5);
pre = ArrayUtils.concatenate(pre, ArrayUtils.intToByteArray(data.length));
pre = ArrayUtils.concatenate(pre, ArrayUtils.intToByteArray(i * CHUNKSIZE));
part = ArrayUtils.concatenate(pre, part);
out = ArrayUtils.concatenate(out, part);
i++;
}
2015-04-13 22:42:16 +02:00
//Decorate the restportion < CHUNKSIZE
byte[] part = Arrays.copyOfRange(data, i * CHUNKSIZE, data.length);
byte[] pre = markAPP1;
pre = ArrayUtils.concatenate(pre, genLen(2+markExtendedXMP.length+32+4+4+part.length));
pre = ArrayUtils.concatenate(pre, markExtendedXMP);
pre = ArrayUtils.concatenate(pre, md5);
pre = ArrayUtils.concatenate(pre, ArrayUtils.intToByteArray(data.length));
pre = ArrayUtils.concatenate(pre, ArrayUtils.intToByteArray(i * CHUNKSIZE));
part = ArrayUtils.concatenate(pre, part);
out = ArrayUtils.concatenate(out, part);
return out;
}
System.err.println("JPEGUtils.decorateBlock(): No valid type entered.");
return null;
}
2015-04-13 22:42:16 +02:00
/**
* Extract the value of a key from the data Array.
* For Example: data = 'blabalkey="Hallo"blabla', key = 'key'
* Output of extract(data, key) will be 'Hallo'
*
2015-04-13 22:42:16 +02:00
* @param data array of bytes
* @param key array that contains the key
* @return the "value" of the key (the part after the key and the following '="' to the next '"'.
2015-04-13 22:42:16 +02:00
*/
public static byte[] extract(byte[] data, byte[] key)
{
int start = -1;
int end = -1;
for (int i = 0; i <= data.length-key.length; i++)
{
if (ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, key, i) && data[i+key.length] == 0x3D && data[i+key.length+1] == 0x22)
{
start = i+key.length+2;
for (int j = i+key.length+2; j < data.length; j++)
{
if (data[j] == 0x22)
{
end = j;
return Arrays.copyOfRange(data, start, end);
}
}
System.err.println("JPEGUtils.extract() found end of key \""+new String(key)+"\": false");
}
}
System.err.println("JPEGUtils.extract() found key \""+new String(key)+"\": false");
return null;
}
2015-04-13 22:42:16 +02:00
/**
* Extract the depth information from a byte array (image as array)
*
2015-04-13 22:42:16 +02:00
* @param data array of a jpeg image
* @return the value of Const.keyGDepthData (GDepth:Data)
*/
public static byte[] extractDepthMap(byte[] data)
{
byte[] meta = getXMPBlocksContent(data);
byte[] depth = extract(meta, keyGDepthData);
if (depth == null) System.err.println("JPEGUtils.extractDepthMap() is null!");
return depth;
}
2015-04-13 22:42:16 +02:00
/**
* Extract the unblurred source image from a byte array (image as array)
*
2015-04-13 22:42:16 +02:00
* @param data array of a jpeg image
* @return the value of Const.keyGImageData (GImage:Data)
*/
public static byte[] extractSourceImage(byte[] data)
{
byte[] meta = getXMPBlocksContent(data);
byte[] src = extract(meta, keyGImageData);
if (src == null) System.err.println("JPEGUtils.extractSourceImage() is null!");
return src;
}
2015-04-13 22:42:16 +02:00
/**
* convert an integer into a two byte representation in base 256
*
2015-04-13 22:42:16 +02:00
* @param l integer
* @return byte array of length two containing two bytes representing l in base 256
*/
public static byte[] genLen(int l)
{
byte[] o = new byte[2];
o[0] = (byte) (l / 256);
o[1] = (byte) (l % 256);
return o;
}
2015-04-13 22:42:16 +02:00
/**
* Get a block of data from an array containing blocks
*
* @param data array of bytes
2015-04-13 22:42:16 +02:00
* @param boundary position of the APP1 marker of the targeted block
* @return the full targeted block
*/
public static byte[] getBlock(byte[] data, int boundary)
{
if (!JPEGUtils.isAPP1Marker(data, boundary))
{
System.err.println("JPEGUtils.getBlock(): Block is no APP1-block!");
return data;
} else
{
2015-04-13 22:42:16 +02:00
return Arrays.copyOfRange(data, boundary, boundary+JPEGUtils.readBlockLength(boundary, data)+2);
}
}
2015-04-13 22:42:16 +02:00
/**
* Same as getBlock, but returns the targeted block without the APP1 Marker and the two bytes containing the length of the block.
*
* @param data byte array
2015-04-13 22:42:16 +02:00
* @param boundary position of the APP1 marker of the targeted block
* @return the targeted block without the APP1 marker and the two bytes containing the length
*/
public static byte[] getBlockWithoutAPP1(byte[] data, int boundary)
{
if (!JPEGUtils.isAPP1Marker(data, boundary))
{
System.err.println("JPEGUtils.getBlockWithoutAPP1(): Block is no APP1-block!");
return null;
} else
return Arrays.copyOfRange(data, boundary+4, boundary+JPEGUtils.readBlockLength(boundary, data)+2);
}
2015-04-13 22:42:16 +02:00
/**
* Same as getBlock but returns the block without any header (APP1, length, EXIF, StandardXMP, ExtendedXMP)
*
2015-04-13 22:42:16 +02:00
* @param data
* @param boundary
* @return
*/
public static byte[] getBlockWithoutHeader(byte[] data, int boundary)
{
if (!JPEGUtils.isAPP1Marker(data, boundary))
{
System.err.println("JPEGUtils.getBlockWithoutHeader(): Block is no APP1-block!");
return null;
} else
{
int offset;
if (ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markStandardXMP, 4+boundary))
offset = 2+2+markStandardXMP.length;
else if (ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markExtendedXMP, 4+boundary))
offset = 2+2+markExtendedXMP.length+32+4+4;
else if (ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markEXIF, 4+boundary))
offset = 2+2+markEXIF.length;
else offset = 4;
return Arrays.copyOfRange(data, boundary+offset, boundary+JPEGUtils.readBlockLength(boundary, data)+2);
}
}
/**
* Return all positions of "FF E1" in the byte[]
2015-04-13 22:42:16 +02:00
* These are the bytes marking h´the beginning of a block
*
* @param data byte array
2015-04-13 22:42:16 +02:00
* @return array of positions of APP1 marker
*/
public static int[] getBoundaries(byte[] data)
{
Vector<Integer> b = new Vector<Integer>();
for (int i = 0; i < data.length; i++)
{
if (JPEGUtils.isAPP1Marker(data, i))
{
b.add(i);
i += 3; //Skip E1 and length
}
}
int[] out = new int[b.size()];
for (int i = 0; i < b.size(); i++)
out[i] = b.get(i);
return out;
}
/**
2015-04-13 22:42:16 +02:00
* Return the exif block of the jpg. This is usually the first block of data, but this tolerates any position
*
2015-04-13 22:42:16 +02:00
* @param data array of bytes
* @return exif block
*/
public static byte[] getEXIFBlock(byte[] data)
{
int[] bounds = getBoundaries(data);
for (int e : bounds)
{
byte[] block = getBlock(data, e);
if (isEXIFBlock(block)) return getBlockWithoutHeader(data, e);
}
System.err.println("JPEGUtils.getEXIFBlock(): No EXIF-block found!");
return null;
}
2015-04-13 22:42:16 +02:00
/**
* Gets the concatenated content of all ExtendedXMPBlocks in the data array without headers
*
2015-04-13 22:42:16 +02:00
* @param data array
* @return content of the blocks
*/
public static byte[] getExtendedXMPBlockContent(byte[] data)
{
int[] bounds = JPEGUtils.getBoundaries(data);
byte[] cont = new byte[0];
for (int e : bounds)
{
byte[] block = JPEGUtils.getBlock(data, e);
if (JPEGUtils.isExtendedXMP(block))
{
byte[] part = JPEGUtils.getBlockWithoutHeader(block, 0);
if (part == null)
System.err.println("JPEGUtils.getExtendedXMPBlockContent(): Part "+e+" is null!");
cont = ArrayUtils.concatenate(cont, part);
}
}
return cont;
}
2015-04-13 22:42:16 +02:00
/**
* Returns the tail of the data array that is not content of any block. In case of the google camera this contains the JPEG image data
*
2015-04-13 22:42:16 +02:00
* @param data byte array
* @return the trailing headless imagedata
2015-04-13 22:42:16 +02:00
*/
public static byte[] getImageTail(byte[] data)
{
byte[] out;
int[] bounds = getBoundaries(data);
if (bounds.length != 0)
{
int offset = 256 * (data[bounds[bounds.length-1]+2] & 0xFF)+(data[bounds[bounds.length-1]+3] & 0xFF);
offset += bounds[bounds.length-1];
offset += 2;
out = Arrays.copyOfRange(data, offset, data.length);
return out;
} else return null;
}
2015-04-13 22:42:16 +02:00
/**
* Returns the concatenated contents of all APP1 Blocks without the APP1 Markers
*
2015-04-13 22:42:16 +02:00
* @param data byte array
* @return the concatenated contents of APP1 Blocks
*/
public static byte[] getMetadata(byte[] data)
{
int[] boundaries = getBoundaries(data);
byte[] out = new byte[0];
for (int e : boundaries)
{
out = ArrayUtils.concatenate(out, getBlockWithoutAPP1(data, e));
}
return out;
}
2015-04-13 22:42:16 +02:00
/**
* Returns the content of the StandardXMPBlock without header.
*
2015-04-13 22:42:16 +02:00
* @param data byte array
* @return the content of the StandardXMPBlock
*/
public static byte[] getStandardXMPBlockContent(byte[] data)
{
int[] bounds = JPEGUtils.getBoundaries(data);
for (int e : bounds)
{
byte[] block = JPEGUtils.getBlock(data, e);
if (JPEGUtils.isStandardXMP(block)) return JPEGUtils.getBlockWithoutHeader(data, e);
}
System.err.println("JPEGUtils.getStandardXMPBlockContent() is null!");
return null;
}
2015-04-13 22:42:16 +02:00
/**
* Returns the content of all XMPBlocks (StandardXMP + ExtendedXMP) without headers and concatenated.
*
2015-04-13 22:42:16 +02:00
* @param data byte array
* @return byte array with contents
*/
public static byte[] getXMPBlocksContent(byte[] data)
{
byte[] stand = getStandardXMPBlockContent(data);
byte[] ext = getExtendedXMPBlockContent(data);
byte[] out = ArrayUtils.concatenate(stand, ext);
if (out == null) System.err.println("JPEGUtils.getXMPBlocksContent() is null!");
return out;
}
2015-04-13 22:42:16 +02:00
/**
* Returns true, if there is a APP1 marker at the given offset in data.
*
* @param data byte array
2015-04-13 22:42:16 +02:00
* @param offset offset
* @return true, if there is an APP1 marker at offset in data, else false
*/
public static boolean isAPP1Marker(byte[] data, int offset)
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markAPP1, offset);
}
2015-04-13 22:42:16 +02:00
/**
* Returns true, if there is an Exif marker after the APP1 marker in this block.
*
2015-04-13 22:42:16 +02:00
* @param block byte array
* @return true, if there is an Exif marker at offset 4, else false.
*/
public static boolean isEXIFBlock(byte[] block)
{
return JPEGUtils.isEXIFMarker(block, 4);
}
2015-04-13 22:42:16 +02:00
/**
* Returns true, if there is an Exif marker at offset in data, else false
*
2015-04-13 22:42:16 +02:00
* @param data
* @param offset
* @return true, if there is an Exif marker at offset in data, else false
*/
public static boolean isEXIFMarker(byte[] data, int offset)
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markEXIF, offset);
}
/**
2015-04-13 22:42:16 +02:00
* Returns true, if block is an extendedXMPBlock.
*
2015-04-13 22:42:16 +02:00
* @param block the block with FFE1 and the two following bytes
* @return true, if there is an extendedXMPmarker at offset 4 in block, else false
*/
public static boolean isExtendedXMP(byte[] block)
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(block, markExtendedXMP, 4);
}
2015-04-13 22:42:16 +02:00
/**
* Returns true, if data has the magical bytes of a jpeg file at the start
*
2015-04-13 22:42:16 +02:00
* @param data byte array
* @return true, if there is a jpegmarker at offset 0 (FFD8)
*/
public static boolean isJPG(byte[] data)
{
return JPEGUtils.isJPG(data, 0);
}
2015-04-13 22:42:16 +02:00
/**
* Returns true, if there is a jpegMarker at offset in data
*
* @param data byte array
2015-04-13 22:42:16 +02:00
* @param offset offset
* @return true, if there is a jpegMarker at offset in data, ellse false
*/
public static boolean isJPG(byte[] data, int offset)
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markJPG, offset);
}
/**
2015-04-13 22:42:16 +02:00
* Returns true, if data is a standardXMPBlock
*
2015-04-13 22:42:16 +02:00
* @param block block with FFE1 and the two following bytes
* @return true, if the block is standardXMP (has standardXMP marker at offset 4), else false
*/
public static boolean isStandardXMP(byte[] block)
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(block, markStandardXMP, 4);
}
2015-04-13 22:42:16 +02:00
/**
* Returns true, if data has a standardXMPMarker at offset offset
*
2015-04-13 22:42:16 +02:00
* @param block byte array
* @return true, if the block has standardXMPMarker at offset, else false
*/
public static boolean isStandardXMP(byte[] block, int offset)
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(block, markStandardXMP, offset);
}
2015-04-13 22:42:16 +02:00
/**
* Replace the value of a key in data with another value value
*
* @param data byte array
* @param key key of the value that will be modified
2015-04-13 22:42:16 +02:00
* @param value new value of key
* @return modified data byte array
*/
public static byte[] replace(byte[] data, byte[] key, byte[] value)
{
int start = -1;
int end = -1;
for (int i = 0; i < data.length; i++)
{
if (ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, key, i))
{
start = i+key.length+2;
break;
}
}
if (start != -1)
{
byte[] pre = Arrays.copyOfRange(data, 0, start);
for (int j = start; j < data.length; j++)
{
if (data[j] == 0x22)
{
end = j;
break;
}
}
if (end != -1)
{
byte[] post = Arrays.copyOfRange(data, end, data.length);
byte[] out = ArrayUtils.concatenate(pre, value);
out = ArrayUtils.concatenate(out, post);
return out;
} else System.err.println("JPEGUtils.replace(): No closing \" found in data!");
} else System.err.println("JPEGUtils.replace(): Key not found in data!");
return null;
}
}