Added Base64Wrapper to support different Base64 Implementaions (like android). Also added some functionality for android compatibility

This commit is contained in:
vanitasvitae 2015-04-28 22:47:56 +02:00
parent d7ccf6883c
commit 7cf965b1f5
9 changed files with 480 additions and 309 deletions

Binary file not shown.

View file

@ -1,9 +1,9 @@
public class ArrayUtils
{
/**
* Return true, if the array part is sub array of src from offset
*
* @param src array
* @param part array
* @param offset int
@ -11,49 +11,53 @@ public class ArrayUtils
*/
public static boolean arrayIsPartOfOtherArrayOnOffset(byte[] src, byte[] part, int offset)
{
if(offset<0 || part.length+offset > src.length) return false;
for(int i=0; i<part.length; i++)
if (offset < 0 || part.length+offset > src.length) return false;
for (int i = 0; i < part.length; i++)
{
if(part[i] != src[i+offset]) return false;
if (part[i] != src[i+offset]) return false;
}
return true;
}
/**
* converts the byte array b into a char array by casting all the bytes into chars
*
* @param b byte array
* @return char array
*/
public static char[] bytesToChars(byte[] b)
{
char[] c = new char[b.length];
for(int i=0; i<b.length; i++)
for (int i = 0; i < b.length; i++)
c[i] = (char) b[i];
return c;
}
/**
* converts the char array into a byte array by casting all the chars into bytes
*
* @param c char array
* @return byte array
*/
public static byte[] charsToBytes(char[] c)
{
byte[] b = new byte[c.length];
for(int i=0; i<c.length; i++)
for (int i = 0; i < c.length; i++)
b[i] = (byte) c[i];
return b;
}
/**
* concatenate two byte arrays
*
* @param first first byte array
* @param second second byte array
* @return first + second
*/
public static byte[] concatenate (byte[] first, byte[] second) {
if(first == null) return second;
if(second == null) return first;
public static byte[] concatenate(byte[] first, byte[] second)
{
if (first == null) return second;
if (second == null) return first;
int aLen = first.length;
int bLen = second.length;
byte[] c = new byte[aLen+bLen];
@ -64,27 +68,30 @@ public class ArrayUtils
/**
* convert an integer into a 32 bit byte array
*
* @param value integer
* @return byte array
*/
public static final byte[] intToByteArray(int value) {
return new byte[] {
(byte)(value >>> 24),
(byte)(value >>> 16),
(byte)(value >>> 8),
(byte)value};
public static final byte[] intToByteArray(int value)
{
return new byte[]{
(byte) (value >>> 24),
(byte) (value >>> 16),
(byte) (value >>> 8),
(byte) value};
}
/**
* convert a signed byte array into an unsigned byte array (sort of)
*
* @param b byte array of signed bytes
* @return byte array of unsigned bytes
*/
public static byte[] unsign(byte[] b)
{
byte[] u = new byte[b.length];
for(int i=0; i<b.length; i++)
u[i] = (byte) (b[i]&0xFF);
for (int i = 0; i < b.length; i++)
u[i] = (byte) (b[i] & 0xFF);
return u;
}
}

9
src/Base64Wrapper.java Normal file
View file

@ -0,0 +1,9 @@
/**
* Created by vanitas on 28.04.15.
*/
public abstract class Base64Wrapper
{
public abstract byte[] decode(byte[] data);
public abstract byte[] encode(byte[] data);
}

View file

@ -1,5 +1,4 @@
public class Const
{
/**
@ -7,7 +6,9 @@ public class Const
*/
public static final int CHUNKSIZE = 65400;
/** Strings for JPEGUtils.decorate() */
/**
* Strings for JPEGUtils.decorate()
*/
public static final String EXIF = "EXIF";
public static final String STANDARDXMP = "StandardXMP";
@ -18,6 +19,11 @@ public class Const
*/
public static byte[] markJPG = {(byte) 0xFF, (byte) 0xD8};
/**
* Magical bytes that idetify the file as PNG. (0x89 P N G) (length: 4)
*/
public static byte[] markPNG = {(byte) 0x89, (byte) 0x50, (byte) 0x4E, (byte) 0x47};
/**
* Marker that defines the beginning of a new block of metadata (FF E1) (length: 2)
*/
@ -59,11 +65,11 @@ public class Const
* (xmpNote:HasExtendedXMP) (length: 22)
*/
public static byte[] keyHasExtendedXMP = {
(byte) 0x78,(byte) 0x6d,(byte) 0x70,(byte) 0x4e,(byte) 0x6f,
(byte) 0x74,(byte) 0x65,(byte) 0x3a,(byte) 0x48,(byte) 0x61,
(byte) 0x73,(byte) 0x45,(byte) 0x78,(byte) 0x74,(byte) 0x65,
(byte) 0x6e,(byte) 0x64,(byte) 0x65,(byte) 0x64,(byte) 0x58,
(byte) 0x4d,(byte) 0x50
(byte) 0x78, (byte) 0x6d, (byte) 0x70, (byte) 0x4e, (byte) 0x6f,
(byte) 0x74, (byte) 0x65, (byte) 0x3a, (byte) 0x48, (byte) 0x61,
(byte) 0x73, (byte) 0x45, (byte) 0x78, (byte) 0x74, (byte) 0x65,
(byte) 0x6e, (byte) 0x64, (byte) 0x65, (byte) 0x64, (byte) 0x58,
(byte) 0x4d, (byte) 0x50
};
/**
@ -71,8 +77,8 @@ public class Const
* (GDepth:Data) (length: 11)
*/
public static byte[] keyGDepthData = {
(byte) 0x47,(byte) 0x44,(byte) 0x65,(byte) 0x70,(byte) 0x74,
(byte) 0x68,(byte) 0x3a,(byte) 0x44,(byte) 0x61,(byte) 0x74,
(byte) 0x47, (byte) 0x44, (byte) 0x65, (byte) 0x70, (byte) 0x74,
(byte) 0x68, (byte) 0x3a, (byte) 0x44, (byte) 0x61, (byte) 0x74,
(byte) 0x61
};
@ -81,8 +87,8 @@ public class Const
* (GImage:Data) (length: 11)
*/
public static byte[] keyGImageData = {
(byte) 0x47,(byte) 0x49,(byte) 0x6d,(byte) 0x61,(byte) 0x67,
(byte) 0x65,(byte) 0x3a,(byte) 0x44,(byte) 0x61,(byte) 0x74,
(byte) 0x47, (byte) 0x49, (byte) 0x6d, (byte) 0x61, (byte) 0x67,
(byte) 0x65, (byte) 0x3a, (byte) 0x44, (byte) 0x61, (byte) 0x74,
(byte) 0x61
};
}

View file

@ -1,47 +1,81 @@
import java.util.Base64;
public class DepthMapNeedle
{
/**
* Interprete the arguments and execute the programm
*
* @param args
*/
public static void main(String[] args)
{
Base64Wrapper wrapper = new Base64Wrapper(){
@Override
public byte[] decode(byte[] data)
{
return Base64.getDecoder().decode(data);
}
@Override
public byte[] encode(byte[] data)
{
return Base64.getEncoder().encode(data);
}
};
//No arguments given or '-h' as first argument -> show help text
if(args.length==0 || args[0].equals("-h")) help();
if (args.length == 0 || args[0].equals("-h")) help();
//export depthmap
if(args.length >= 2 && args[0].equals("-e"))
if (args.length >= 2 && args[0].equals("-d"))
{
for(int i=1; i<args.length; i++)
for (int i = 1; i < args.length; i++)
{
JPEG image = new JPEG(args[i]);
if(image.exportDepthMap()) System.out.println("Depthmap extracted for file " + args[i]+".");
JPEG image = new JPEG(args[i], wrapper);
if (image.exportDepthMap())
System.out.println("Depthmap extracted for file "+args[i]+".");
else System.err.println("There is no Depthmap in file "+args[i]);
}
}
//export source image
else if(args.length >=2 && args[0].equals("-s"))
else if (args.length >= 2 && args[0].equals("-s"))
{
for(int i=1; i<args.length; i++)
for (int i = 1; i < args.length; i++)
{
JPEG image = new JPEG(args[i]);
if(image.exportSourceImage()) System.out.println("Unblurred source image extracted for file "+args[i]+".");
else System.err.println("There is no unblurred source image in file "+args[i]+". Maybe this photo has not been taken with the blur function?");
JPEG image = new JPEG(args[i], wrapper);
if (image.exportSourceImage())
System.out.println("Unblurred source image extracted for file "+args[i]+".");
else
System.err.println("There is no unblurred source image in file "+args[i]+". Maybe this photo has not been taken with the blur function?");
}
}
//inject depthmap
else if(args.length >= 3 && args[0].equals("-i"))
else if (args.length >= 3 && args[0].equals("-D"))
{
String depthmap = args[1];
for(int i=2; i<args.length; i++)
for (int i = 2; i < args.length; i++)
{
JPEG image = new JPEG(args[i]);
if(image.injectDepthMap(depthmap)) System.out.println("Depthmap injected into file "+args[i]+".");
else System.err.println("Something went wrong while injecting "+depthmap+" into "+args[i]+".\nRemember: The first argument has to be a png and the following arguments must be jpgs shot with the blur function.");
JPEG image = new JPEG(args[i], wrapper);
if (image.injectDepthMap(depthmap))
System.out.println("Depthmap injected into file "+args[i]+".");
else
System.err.println("Something went wrong while injecting "+depthmap+" into "+args[i]+".\nRemember: The first argument has to be a png and the following arguments must be jpgs shot with the blur function.");
image.save();
}
}
//inject source image
else if (args.length >= 3 && args[0].equals("-S"))
{
String source = args[1];
for (int i = 2; i < args.length; i++)
{
JPEG image = new JPEG(args[i], wrapper);
if (image.injectSourceImage(source))
System.out.println("Source image injected into file "+args[i]+".");
else
System.err.println("Something went wrong while injecting "+source+" into "+args[i]+".\nRemember: The first argument has to be a jpg and the following arguments must be jpgs shot with the blur function.");
image.save();
}
}
@ -53,16 +87,19 @@ public class DepthMapNeedle
public static void help()
{
System.out.println("Welcome to DepthMapNeedle!"
+ "\nDepthMapNeedle is a tool to inject or extract depth information in form of depthmaps from photos shot using Google Cameras Blur function."
+ "\n"
+ "\nAvailable Options:"
+ "\n'-e <file1>.jpg ... <fileN>.jpg':"
+ "\n Extract the depthmap from the specified photo(s). The depthmaps will be stored with the suffix \"_d.png\"."
+ "\n'-s <file1>.jpg ... <fileN>.jpg':"
+ "\n Extract the unblurred source image from the photo(s). These files will be stored with the suffix \"_s.jpg\"."
+ "\n'-i <depthmap>.png <file1>.jpg <fileN>.jpg':"
+ "\n Inject a png file as Depthmap into the following specified jpg files."
+ "\n'-h':"
+ "\n Show this help text.");
+"\nDepthMapNeedle is a tool to inject or extract depth information in form of depthmaps from photos shot using Google Cameras Blur function."
+"\n"
+"\nAvailable Options:"
+"\n'-d <file1>.jpg ... <fileN>.jpg':"
+"\n Extract the depthmap from the specified photo(s). The depthmaps will be stored with the suffix \"_d.png\"."
+"\n'-s <file1>.jpg ... <fileN>.jpg':"
+"\n Extract the unblurred source image from the photo(s). These files will be stored with the suffix \"_s.jpg\"."
+"\n'-D <depthmap>.png <file1>.jpg ... <fileN>.jpg':"
+"\n Inject a png file as Depthmap into the following specified jpg files."
+"\n'-S <unblurred>.jpg <file1>.jpg ... <fileN>.jpg':"
+"\n Inject an unblurred source image into the following specified jpg files. "
+"\n Note: The unblurred source image is a simple jpg file of the same dimensions as the photos."
+"\n'-h':"
+"\n Show this help text.");
}
}

View file

@ -1,5 +1,4 @@
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@ -7,19 +6,22 @@ public class HexUtil
{
/**
* Print out a byte array in hexadecimal
*
* @param bytes array
*/
public static void printHex(byte[] bytes)
{
for(byte b : bytes) System.out.print(byteToHexString(b));
for (byte b : bytes) System.out.print(byteToHexString(b));
}
/**
* convert a byte to a hex string
*
* @param data array
* @return String representation in hex
*/
public static String byteToHexString(byte data) {
public static String byteToHexString(byte data)
{
StringBuffer buf = new StringBuffer();
buf.append(toHexChar((data >>> 4) & 0x0F));
@ -30,19 +32,24 @@ public class HexUtil
/**
* convert a integer into a hexadecimal character
*
* @param i integer
* @return hex char
*/
public static char toHexChar(int i) {
if ((0 <= i) && (i <= 9)) {
return (char) ('0' + i);
} else {
return (char) ('a' + (i - 10));
public static char toHexChar(int i)
{
if ((0 <= i) && (i <= 9))
{
return (char) ('0'+i);
} else
{
return (char) ('a'+(i-10));
}
}
/**
* Generate the md5 digest of the byte array data in hexadecimal
*
* @param data array
* @return byte array of the md5 digest in hex
*/
@ -51,10 +58,10 @@ public class HexUtil
try
{
byte[] md5 = MessageDigest.getInstance("MD5").digest(data);
String m="";
for(int i=0; i<md5.length; i++)
String m = "";
for (int i = 0; i < md5.length; i++)
{
m = m + byteToHexString(md5[i]).toUpperCase();
m = m+byteToHexString(md5[i]).toUpperCase();
}
return m.getBytes();
} catch (NoSuchAlgorithmException e)

View file

@ -1,5 +1,4 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -10,10 +9,12 @@ public class IO
{
/**
* Read file from disk to byte array
*
* @param file path to file
* @return byte array
*/
public static byte[] read(File file) {
public static byte[] read(File file)
{
try
{
InputStream is = new FileInputStream(file);
@ -21,8 +22,7 @@ public class IO
is.read(b);
is.close();
return b;
}
catch (IOException e)
} catch (IOException e)
{
System.err.println("Couldn't read file "+file.getAbsolutePath());
e.printStackTrace();
@ -32,6 +32,7 @@ public class IO
/**
* Write byte array to file on disk
*
* @param b byte array
* @param filepath path to outputfile
*/
@ -40,7 +41,7 @@ public class IO
FileOutputStream fos;
try
{
fos=new FileOutputStream(filepath);
fos = new FileOutputStream(filepath);
fos.write(b);
fos.close();
} catch (Exception e)

View file

@ -1,6 +1,5 @@
import java.io.File;
import java.util.Base64;
public class JPEG extends Const
{
@ -10,50 +9,56 @@ public class JPEG extends Const
/**
* location of the source file
*/
private String filename;
private String m_Filename;
/**
* byte array containing the actual file
*/
private byte[] rawData;
private byte[] m_RawData;
/**
* byte array containing EXIF block information without header
*/
private byte[] exif;
private byte[] m_Exif;
/**
* byte array containing StandardXMP block information without header
*/
private byte[] xmpSta;
private byte[] m_StandardXMP;
/**
* byte array containing ExtendedXMP block information without header
*/
private byte[] xmpExt;
private byte[] m_ExtendedXMP;
/**
* byte array conatining the trailing headless data block conatining the blurred jpeg image
* byte array containing the trailing headless data block containing the blurred jpeg image
*/
private byte[] tail;
private byte[] m_ImageTail;
private Base64Wrapper base64;
/** METHODS */
/**
* Constructor
* @param rawData
*
* @param rawData raw byte array
*/
public JPEG(byte[] rawData)
public JPEG(byte[] rawData, Base64Wrapper wrapper)
{
this.base64 = wrapper;
this.init(rawData, null);
}
/**
* Constructor, that reads a byte array from file
* @param filename
*
* @param filename raw byte array
*/
public JPEG(String filename)
public JPEG(String filename, Base64Wrapper wrapper)
{
this.base64 = wrapper;
this.init(IO.read(new File(filename)), filename);
}
@ -62,246 +67,311 @@ public class JPEG extends Const
*/
public void disassemble()
{
exif = this.getEXIFBlock();
xmpSta = this.getStandardXMPBlockContent();
xmpExt = this.getExtendedXMPContent();
tail = this.getImageTail();
m_Exif = this.getEXIFBlock();
m_StandardXMP = this.getStandardXMPBlockContent();
m_ExtendedXMP = this.getExtendedXMPContent();
m_ImageTail = this.getImageTail();
}
/**
* extract the depthmap from the jpeg and store it in the same location as the jpeg itself but with the suffix _d.png
* extract the depth map from the jpeg and store it in the same location as the jpeg itself but with the suffix _d.png
*
* @return
*/
public boolean exportDepthMap()
{
String out = filename;
if(out.endsWith(".jpg") || out.endsWith(".JPG")) out = out.substring(0, out.length()-4);
String out = m_Filename;
if (out.endsWith(".jpg") || out.endsWith(".JPG")) out = out.substring(0, out.length()-4);
return this.exportDepthMap(out+"_d.png");
}
/**
* extract the depthmap from the jpeg and store it under the location specified in "file"
*
* @param file String of the output location
* @return success
*/
public boolean exportDepthMap(String file)
{
byte[] b64 = ArrayUtils.unsign(extractDepthMap());
byte[] depth = Base64.getDecoder().decode(b64);
if(depth != null)
byte[] depth = base64.decode(b64);
if (depth != null)
{
IO.write(depth, file);
return true;
}
else return false;
} else return false;
}
/**
* Extract and save the unblurred source image. The image will be saved in the same location as the jpeg itself, but with the suffix "_s.jpg"
*
* @return success
*/
public boolean exportSourceImage()
{
String out = filename;
if(out.endsWith(".jpg") || out.endsWith(".JPG")) out = out.substring(0, out.length()-4);
String out = m_Filename;
if (out.endsWith(".jpg") || out.endsWith(".JPG")) out = out.substring(0, out.length()-4);
return this.exportSourceImage(out+"_s.jpg");
}
/**
* Extract the unblurred source image and save it under the location specified in "file"
*
* @param file String of the location
* @return success
*/
public boolean exportSourceImage(String file)
{
byte[] b64 = ArrayUtils.unsign(extractSourceImage());
byte[] src = Base64.getDecoder().decode(b64);
if(src != null)
byte[] src = base64.decode(b64);
if (src != null)
{
IO.write(src, file);
return true;
}
else return false;
} else return false;
}
/**
* Extract and return the depthmap information
*
* @return depthmap as byte array
*/
public byte[] extractDepthMap()
{
return JPEGUtils.extractDepthMap(rawData);
return JPEGUtils.extractDepthMap(m_RawData);
}
/**
* Extract and return the unblurred source image
*
* @return source image as byte array
*/
public byte[] extractSourceImage()
{
return JPEGUtils.extractSourceImage(rawData);
return JPEGUtils.extractSourceImage(m_RawData);
}
/**
* Find all indizes of APP1 markers in the jpegs byte array
*
* @return integer array of all positions of the APP1 marker in the rawData array
*/
public int[] getBoundaries()
{
return JPEGUtils.getBoundaries(rawData);
return JPEGUtils.getBoundaries(m_RawData);
}
/**
* return the exif block of the jpeg as a byte array
*
* @return exifblock (member)
*/
public byte[] getExif()
{
return exif;
return m_Exif;
}
/**
* Gets the exif block from the rawData array instead from the member
*
* @return the exif block from rawData
*/
public byte[] getEXIFBlock()
{
return JPEGUtils.getEXIFBlock(rawData);
return JPEGUtils.getEXIFBlock(m_RawData);
}
/**
* Extract and return the content of all the ExtendedXMP blocks concatenated.
*
* @return content of the ExtendedXMP blocks
*/
public byte[] getExtendedXMPContent()
{
return JPEGUtils.getExtendedXMPBlockContent(rawData);
return JPEGUtils.getExtendedXMPBlockContent(m_RawData);
}
/**
* return the filename and path
* @return
*
* @return member filename
*/
public String getFilename()
{
return filename;
return m_Filename;
}
/**
* Return the headless block of data in the image (this contains the JPEG you see when opening the file)
*
* @return headless JPEG tail
*/
public byte[] getImageTail()
{
return JPEGUtils.getImageTail(rawData);
return JPEGUtils.getImageTail(m_RawData);
}
/**
* Return the Metadata of the image (content of all APP1 marked blocks)
*
* @return metadata
*/
public byte[] getMetadata()
{
return JPEGUtils.getMetadata(rawData);
return JPEGUtils.getMetadata(m_RawData);
}
/**
* Returns the byte array that IO.read(image) returned
*
* @return byte array of the image file
*/
public byte[] getRawData()
{
return this.rawData;
return this.m_RawData;
}
/**
* Extracts and returns the content of the StandardXMP block
*
* @return content of the StandardXMP block
*/
public byte[] getStandardXMPBlockContent()
{
return JPEGUtils.getStandardXMPBlockContent(rawData);
return JPEGUtils.getStandardXMPBlockContent(m_RawData);
}
/**
* Returns the Image tail (member)
*
* @return member image tail
*/
public byte[] getTail()
{
return tail;
return m_ImageTail;
}
/**
* Extracts and returns the content of all XMP blocks concatenated (StandardXMP + ExtendedXMPs)
*
* @return content of the XMP blocks
*/
public byte[] getXMPBlocksContent()
{
return JPEGUtils.getXMPBlocksContent(rawData);
return JPEGUtils.getXMPBlocksContent(m_RawData);
}
/**
* Return the member containing the ExtendedXMP information
*
* @return XmpExt (member)
*/
public byte[] getXmpExt()
{
return xmpExt;
return m_ExtendedXMP;
}
/**
* Return the member containing the StandardXMP information
*
* @return xmpSta (member)
*/
public byte[] getXmpSta()
{
return xmpSta;
return m_StandardXMP;
}
/**
* initializes the JPEG object.
* set the array containing the image as member rawData, set filename
* disassemble the Image and set the other members with content.
*
* @param raw byte array of the image
* @param filename location
*/
private void init(byte[] raw, String filename)
{
this.rawData = raw;
this.filename = filename;
if(ArrayUtils.arrayIsPartOfOtherArrayOnOffset(raw, Const.markJPG,0))
{
this.m_RawData = raw;
this.m_Filename = filename;
this.disassemble();
}
}
/**
* Inject a new depthmap into the jpeg ("replace" the old GDepth:Data value with the Base64 encoded png file)
*
* @param filename location of the new depthmap png
* @return success
*/
public boolean injectDepthMap(String filename)
{
byte[] depth = Base64.getEncoder().encode(IO.read(new File(filename)));
xmpExt = JPEGUtils.replace(xmpExt, keyGDepthData, depth);
if(xmpExt != null) return true;
else return false;
byte[] depth = base64.encode(IO.read(new File(filename)));
byte[] newExtendedXMP = JPEGUtils.replace(m_ExtendedXMP, keyGDepthData, depth);
if (newExtendedXMP != null)
{
m_ExtendedXMP = newExtendedXMP;
return true;
} else return false;
}
public boolean injectDepthMap(byte[] data)
{
byte[] depth = base64.encode(data);
byte[] newExtendedXMP = JPEGUtils.replace(m_ExtendedXMP, keyGDepthData, depth);
if (newExtendedXMP != null)
{
m_ExtendedXMP = newExtendedXMP;
return true;
} else return false;
}
/**
* Inject a new unblurred source image into the jpeg ("replace" the old GImage:Data value with the Base64 encoded jpg file)
*
* @param filename location of the new unblurred source image jpg
* @return success
*/
public boolean injectSourceImage(String filename)
{
byte[] image = base64.encode(IO.read(new File(filename)));
byte[] newExtendedXMP = JPEGUtils.replace(m_ExtendedXMP, keyGImageData, image);
if (newExtendedXMP != null)
{
m_ExtendedXMP = newExtendedXMP;
return true;
} else return false;
}
public boolean injectSourceImage(byte[] data)
{
byte[] image = base64.encode(data);
byte[] newExtendedXMP = JPEGUtils.replace(m_ExtendedXMP, keyGImageData, image);
if (newExtendedXMP != null)
{
m_ExtendedXMP = newExtendedXMP;
return true;
} else return false;
}
/**
* Reassemble a functional jpeg byte array from block data.
* Set rawData to the new array and also return it
*
* @return reassembled jpeg byte array
*/
public byte[] reassemble()
{
byte[] md5 = HexUtil.generateMD5(xmpExt);
byte[] md5 = HexUtil.generateMD5(m_ExtendedXMP);
byte[] out = markJPG;
out = ArrayUtils.concatenate(out, JPEGUtils.decorateBlock(exif,EXIF));
out = ArrayUtils.concatenate(out, JPEGUtils.decorateBlock(xmpSta, STANDARDXMP));
out = ArrayUtils.concatenate(out, JPEGUtils.decorateBlock(xmpExt, EXTENDEDXMP));
out = ArrayUtils.concatenate(out, tail);
out = ArrayUtils.concatenate(out, JPEGUtils.decorateBlock(m_Exif, EXIF));
out = ArrayUtils.concatenate(out, JPEGUtils.decorateBlock(m_StandardXMP, STANDARDXMP));
out = ArrayUtils.concatenate(out, JPEGUtils.decorateBlock(m_ExtendedXMP, EXTENDEDXMP));
out = ArrayUtils.concatenate(out, m_ImageTail);
out = JPEGUtils.replace(out, keyHasExtendedXMP, md5);
this.rawData = out;
this.m_RawData = out;
return out;
}
@ -310,11 +380,12 @@ public class JPEG extends Const
*/
public void save()
{
this.save(filename);
this.save(m_Filename);
}
/**
* Write the image to the file specified in "file"
*
* @param file
*/
public void save(String file)
@ -323,38 +394,42 @@ public class JPEG extends Const
}
/**
* Set the exif memberto a new byte array
* Set the exif member to a new byte array
*
* @param exif new array
*/
public void setExif(byte[] exif)
{
this.exif=exif;
this.m_Exif = exif;
}
/**
* Set the tail member array to a new byte array
*
* @param tail new array
*/
public void setTail(byte[] tail)
{
this.tail=tail;
this.m_ImageTail = tail;
}
/**
* set the xmpExt member array to a new byte array
*
* @param xmpExt new array
*/
public void setXmpExt(byte[] xmpExt)
{
this.xmpExt=xmpExt;
this.m_ExtendedXMP = xmpExt;
}
/**
* set the xmpSta member array to a new byte array
*
* @param xmpSta new array
*/
public void setXmpSta(byte[] xmpSta)
{
this.xmpSta=xmpSta;
this.m_StandardXMP = xmpSta;
}
}

View file

@ -1,5 +1,4 @@
import java.util.Arrays;
import java.util.Vector;
@ -7,6 +6,7 @@ public class JPEGUtils extends Const
{
/**
* Read the length of the block from the two bytes after the APP1 Marker
*
* @param boundaryPos position of the APP1 Marker in the byte array
* @param data byte array containing the block WITH APP1 Marker
* @return length of the block
@ -14,7 +14,7 @@ public class JPEGUtils extends Const
public static int readBlockLength(int boundaryPos, byte[] data)
{
//Check whether entered position is APP1 Marker
if(!JPEGUtils.isAPP1Marker(data,boundaryPos))
if (!JPEGUtils.isAPP1Marker(data, boundaryPos))
{
System.err.println("JPEGUtils blockLength: Block is no APP1 Block!");
return -1;
@ -23,9 +23,8 @@ public class JPEGUtils extends Const
//Read length
try
{
o = 256*(data[boundaryPos+2]&0xFF) + (data[boundaryPos+3]&0xFF);
}
catch(ArrayIndexOutOfBoundsException e)
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();
@ -39,6 +38,7 @@ public class JPEGUtils extends Const
* In case, type == EXTENDEDXMP and data.length > CHUNKSIZE multiple ExtendedXMPBlocks will be generated and concatenated.
* Available types:
* Const.EXIF, Const.STANDARDXMP, Const.EXTENDEDXMP
*
* @param data byte array of data that will be decorated
* @param type String declaring the type of header for the block.
* @return decorated block
@ -46,49 +46,49 @@ public class JPEGUtils extends Const
public static byte[] decorateBlock(byte[] data, String type)
{
//EXIF Block: 'APP1 + BLOCKLENGTH + EXIF\0\0 + data'
if(type.equals(EXIF))
if (type.equals(EXIF))
{
data = ArrayUtils.concatenate(markEXIF, data);
byte[] pre = ArrayUtils.concatenate(markAPP1, genLen(data.length+2));
return ArrayUtils.concatenate(pre, data);
}
//StandardXMP: 'APP1 + BLOCKLENGTH + http://ns.adobe.com/xap/1.0/\0 + data'
else if(type.equals(STANDARDXMP))
else if (type.equals(STANDARDXMP))
{
data = ArrayUtils.concatenate(markStandardXMP, data);
byte[] pre = ArrayUtils.concatenate(markAPP1, genLen(data.length+2));
return ArrayUtils.concatenate(pre,data);
return ArrayUtils.concatenate(pre, data);
}
//ExtendedXMP: 'APP1 + BLOCKLENGTH + http://ns.adobe.com/xmp/extension/\0 + MD5 + EXTENDEDLENGTH + EXTENDEDOFFSET + DATAPORTION
else if(type.equals(EXTENDEDXMP))
else if (type.equals(EXTENDEDXMP))
{
byte[] out = new byte[0];
//MD5 checksum is digest of the datacontent
byte[] md5 = HexUtil.generateMD5(data);
int i=0;
int blockCount = data.length/CHUNKSIZE;
int i = 0;
int blockCount = data.length / CHUNKSIZE;
//decorate blockportions of size CHUNKSIZE
while (i<blockCount)
while (i < blockCount)
{
byte[] part = Arrays.copyOfRange(data, i*CHUNKSIZE, (i+1)*CHUNKSIZE);
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, 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));
pre = ArrayUtils.concatenate(pre, ArrayUtils.intToByteArray(i * CHUNKSIZE));
part = ArrayUtils.concatenate(pre, part);
out = ArrayUtils.concatenate(out, part);
i++;
}
//Decorate the restportion < CHUNKSIZE
byte[] part = Arrays.copyOfRange(data, i*CHUNKSIZE, data.length);
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, 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));
pre = ArrayUtils.concatenate(pre, ArrayUtils.intToByteArray(i * CHUNKSIZE));
part = ArrayUtils.concatenate(pre, part);
out = ArrayUtils.concatenate(out, part);
return out;
@ -101,6 +101,7 @@ public class JPEGUtils extends Const
* 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'
*
* @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 '"'.
@ -109,29 +110,30 @@ public class JPEGUtils extends Const
{
int start = -1;
int end = -1;
for(int i=0; i<=data.length-key.length; i++)
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)
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++)
for (int j = i+key.length+2; j < data.length; j++)
{
if(data[j] == 0x22)
if (data[j] == 0x22)
{
end = j;
return Arrays.copyOfRange(data, start, end);
}
}
System.err.println("JPEGUtils extract found end for \""+new String(key) +"\": false");
System.err.println("JPEGUtils extract found end for \""+new String(key)+"\": false");
}
}
System.err.println("JPEGUtils extract found start for \""+new String(key) +"\": false");
System.err.println("JPEGUtils extract found start for \""+new String(key)+"\": false");
return null;
}
/**
* Extract the depth information from a byte array (image as array)
*
* @param data array of a jpeg image
* @return the value of Const.keyGDepthData (GDepth:Data)
*/
@ -139,12 +141,13 @@ public class JPEGUtils extends Const
{
byte[] meta = getXMPBlocksContent(data);
byte[] depth = extract(meta, keyGDepthData);
if(depth == null) System.err.println("JPEGUtils extractDepthMap is null");
if (depth == null) System.err.println("JPEGUtils extractDepthMap is null");
return depth;
}
/**
* Extract the unblurred source image from a byte array (image as array)
*
* @param data array of a jpeg image
* @return the value of Const.keyGImageData (GImage:Data)
*/
@ -152,37 +155,38 @@ public class JPEGUtils extends Const
{
byte[] meta = getXMPBlocksContent(data);
byte[] src = extract(meta, keyGImageData);
if(src == null) System.err.println("JPEGUtils extractSourceImage is null");
if (src == null) System.err.println("JPEGUtils extractSourceImage is null");
return src;
}
/**
* convert an integer into a two byte representation in base 256
*
* @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);
o[0] = (byte) (l / 256);
o[1] = (byte) (l % 256);
return o;
}
/**
* Get a block of data from an array containing blocks
*
* @param data array of bytes
* @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))
if (!JPEGUtils.isAPP1Marker(data, boundary))
{
System.err.println("JPEGUtils getBlock: Block is no APP1-block");
return data;
}
else
} else
{
return Arrays.copyOfRange(data, boundary, boundary+JPEGUtils.readBlockLength(boundary, data)+2);
}
@ -190,79 +194,87 @@ public class JPEGUtils extends Const
/**
* 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
* @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))
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);
} else
return Arrays.copyOfRange(data, boundary+4, boundary+JPEGUtils.readBlockLength(boundary, data)+2);
}
/**
* Same as getBlock but returns the block without any header (APP1, length, EXIF, StandardXMP, ExtendedXMP)
*
* @param data
* @param boundary
* @return
*/
public static byte[] getBlockWithoutHeader(byte[] data, int boundary)
{
if(!JPEGUtils.isAPP1Marker(data,boundary))
if (!JPEGUtils.isAPP1Marker(data, boundary))
{
System.err.println("JPEGUtils getBlockWithoutHeader: Block is no APP1-block");
return null;
}
else
} 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;
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 Arrays.copyOfRange(data, boundary+offset, boundary+JPEGUtils.readBlockLength(boundary, data)+2);
}
}
/**
* Return all positions of "FF E1" in the byte[]
* These are the bytes marking h´the beginning of a block
*
* @param data byte array
* @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++)
for (int i = 0; i < data.length; i++)
{
if(JPEGUtils.isAPP1Marker(data,i))
if (JPEGUtils.isAPP1Marker(data, i))
{
b.add(i);
i+=3; //Skip E1 and length
i += 3; //Skip E1 and length
}
}
int[] out = new int[b.size()];
for(int i=0; i<b.size(); i++)
for (int i = 0; i < b.size(); i++)
out[i] = b.get(i);
return out;
}
/**
* Return the exif block of the jpg. This is usually the first block of data, but this tolerates any position
*
* @param data array of bytes
* @return exif block
*/
public static byte[] getEXIFBlock(byte[] data)
{
int[] bounds = getBoundaries(data);
for(int e : bounds)
for (int e : bounds)
{
byte[] block = getBlock(data, e);
if(isEXIFBlock(block)) return getBlockWithoutHeader(data,e);
if (isEXIFBlock(block)) return getBlockWithoutHeader(data, e);
}
System.err.println("JPEGUtils getEXIFBlock: No EXIF-block found");
return null;
@ -270,6 +282,7 @@ public class JPEGUtils extends Const
/**
* Gets the concatenated content of all ExtendedXMPBlocks in the data array without headers
*
* @param data array
* @return content of the blocks
*/
@ -277,13 +290,14 @@ public class JPEGUtils extends Const
{
int[] bounds = JPEGUtils.getBoundaries(data);
byte[] cont = new byte[0];
for(int e : bounds)
for (int e : bounds)
{
byte[] block = JPEGUtils.getBlock(data,e);
if(JPEGUtils.isExtendedXMP(block))
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 is null");
byte[] part = JPEGUtils.getBlockWithoutHeader(block, 0);
if (part == null)
System.err.println("JPEGUtils getExtendedXMPBlockContent part is null");
cont = ArrayUtils.concatenate(cont, part);
}
}
@ -292,22 +306,27 @@ public class JPEGUtils extends Const
/**
* 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
*
* @param data byte array
* @return the trailing blockless imagedata
* @return the trailing headless imagedata
*/
public static byte[] getImageTail(byte[] data)
{
byte[] out;
int[] bounds = getBoundaries(data);
int offset = 256*(data[bounds[bounds.length-1]+2]&0xFF) + (data[bounds[bounds.length-1]+3]&0xFF);
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;
offset += 2;
out = Arrays.copyOfRange(data, offset, data.length);
return out;
} else return null;
}
/**
* Returns the concatenated contents of all APP1 Blocks without the APP1 Markers
*
* @param data byte array
* @return the concatenated contents of APP1 Blocks
*/
@ -315,25 +334,26 @@ public class JPEGUtils extends Const
{
int[] boundaries = getBoundaries(data);
byte[] out = new byte[0];
for(int e : boundaries)
for (int e : boundaries)
{
out = ArrayUtils.concatenate(out, getBlockWithoutAPP1(data,e));
out = ArrayUtils.concatenate(out, getBlockWithoutAPP1(data, e));
}
return out;
}
/**
* Returns the content of the StandardXMPBlock without header.
*
* @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)
for (int e : bounds)
{
byte[] block = JPEGUtils.getBlock(data, e);
if(JPEGUtils.isStandardXMP(block)) return JPEGUtils.getBlockWithoutHeader(data,e);
if (JPEGUtils.isStandardXMP(block)) return JPEGUtils.getBlockWithoutHeader(data, e);
}
System.err.println("JPEGUtils getStandardXMPBlockContent is null");
return null;
@ -341,6 +361,7 @@ public class JPEGUtils extends Const
/**
* Returns the content of all XMPBlocks (StandardXMP + ExtendedXMP) without headers and concatenated.
*
* @param data byte array
* @return byte array with contents
*/
@ -350,12 +371,13 @@ public class JPEGUtils extends Const
byte[] ext = getExtendedXMPBlockContent(data);
byte[] out = ArrayUtils.concatenate(stand, ext);
if(out==null) System.err.println("JPEGUtils getXMPBlocksContent is null");
if (out == null) System.err.println("JPEGUtils getXMPBlocksContent is null");
return out;
}
/**
* Returns true, if there is a APP1 marker at the given offset in data.
*
* @param data byte array
* @param offset offset
* @return true, if there is an APP1 marker at offset in data, else false
@ -367,6 +389,7 @@ public class JPEGUtils extends Const
/**
* Returns true, if there is an Exif marker after the APP1 marker in this block.
*
* @param block byte array
* @return true, if there is an Exif marker at offset 4, else false.
*/
@ -377,6 +400,7 @@ public class JPEGUtils extends Const
/**
* Returns true, if there is an Exif marker at offset in data, else false
*
* @param data
* @param offset
* @return true, if there is an Exif marker at offset in data, else false
@ -388,6 +412,7 @@ public class JPEGUtils extends Const
/**
* Returns true, if block is an extendedXMPBlock.
*
* @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
*/
@ -398,6 +423,7 @@ public class JPEGUtils extends Const
/**
* Returns true, if data has the magical bytes of a jpeg file at the start
*
* @param data byte array
* @return true, if there is a jpegmarker at offset 0 (FFD8)
*/
@ -408,6 +434,7 @@ public class JPEGUtils extends Const
/**
* Returns true, if there is a jpegMarker at offset in data
*
* @param data byte array
* @param offset offset
* @return true, if there is a jpegMarker at offset in data, ellse false
@ -416,8 +443,10 @@ public class JPEGUtils extends Const
{
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markJPG, offset);
}
/**
* Returns true, if data is a standardXMPBlock
*
* @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
*/
@ -428,6 +457,7 @@ public class JPEGUtils extends Const
/**
* Returns true, if data has a standardXMPMarker at offset offset
*
* @param block byte array
* @return true, if the block has standardXMPMarker at offset, else false
*/
@ -438,6 +468,7 @@ public class JPEGUtils extends Const
/**
* 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
* @param value new value of key
@ -447,35 +478,33 @@ public class JPEGUtils extends Const
{
int start = -1;
int end = -1;
for(int i=0; i<data.length; i++)
for (int i = 0; i < data.length; i++)
{
if(ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, key, i))
if (ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, key, i))
{
start = i+key.length+2;
break;
}
}
if(start != -1)
if (start != -1)
{
byte[] pre = Arrays.copyOfRange(data, 0, start);
for(int j=start; j<data.length; j++)
for (int j = start; j < data.length; j++)
{
if(data[j] == 0x22)
if (data[j] == 0x22)
{
end = j;
break;
}
}
if(end != -1)
if (end != -1)
{
byte[] post = Arrays.copyOfRange(data, end, data.length);
byte[] out = ArrayUtils.concatenate(pre, value);
out = ArrayUtils.concatenate(out,post);
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");
} else System.err.println("JPEGUtils replace: No closing \" found in data");
} else System.err.println("JPEGUtils replace: key not found in data");
return null;
}
}