Initial Commit. Added all the source files. Enjoy ;)
This commit is contained in:
parent
de0ad57063
commit
d6f7eb9591
8 changed files with 877 additions and 0 deletions
58
src/ArrayUtils.java
Normal file
58
src/ArrayUtils.java
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
|
||||||
|
|
||||||
|
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(part[i] != src[i+offset]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static char[] bytesToChars(byte[] b)
|
||||||
|
{
|
||||||
|
char[] c = new char[b.length];
|
||||||
|
for(int i=0; i<b.length; i++)
|
||||||
|
c[i] = (char) b[i];
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] charsToBytes(char[] c)
|
||||||
|
{
|
||||||
|
byte[] b = new byte[c.length];
|
||||||
|
for(int i=0; i<c.length; i++)
|
||||||
|
b[i] = (byte) c[i];
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] concatenate (byte[] out, byte[] bs) {
|
||||||
|
if(out == null) return bs;
|
||||||
|
if(bs == null) return out;
|
||||||
|
int aLen = out.length;
|
||||||
|
int bLen = bs.length;
|
||||||
|
byte[] c = new byte[aLen+bLen];
|
||||||
|
System.arraycopy(out, 0, c, 0, aLen);
|
||||||
|
System.arraycopy(bs, 0, c, aLen, bLen);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final byte[] intToByteArray(int value) {
|
||||||
|
return new byte[] {
|
||||||
|
(byte)(value >>> 24),
|
||||||
|
(byte)(value >>> 16),
|
||||||
|
(byte)(value >>> 8),
|
||||||
|
(byte)value};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return u;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
42
src/Const.java
Normal file
42
src/Const.java
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
|
||||||
|
|
||||||
|
public class Const
|
||||||
|
{
|
||||||
|
public static final int CHUNKSIZE = 65400;
|
||||||
|
|
||||||
|
public static final String EXIF = "EXIF";
|
||||||
|
public static final String STANDARDXMP = "StandardXMP";
|
||||||
|
public static final String EXTENDEDXMP = "ExtendedXMP";
|
||||||
|
|
||||||
|
public static byte[] markJPG = {(byte) 0xFF, (byte) 0xD8}; //FFD8
|
||||||
|
|
||||||
|
public static byte[] markAPP1 = {(byte) 0xFF, (byte) 0xE1}; //FFE1
|
||||||
|
|
||||||
|
public static byte[] markEXIF = {(byte) 0x45, (byte) 0x78, (byte) 0x69, (byte) 0x66, (byte) 0x00, (byte) 0x00}; //EXIF\0\0
|
||||||
|
|
||||||
|
public static byte[] markStandardXMP = { //Length: 29
|
||||||
|
(byte) 0x68, (byte) 0x74, (byte) 0x74, (byte) 0x70, // http
|
||||||
|
(byte) 0x3A, (byte) 0x2F, (byte) 0x2F, (byte) 0x6E, (byte) 0x73, // ://ns
|
||||||
|
(byte) 0x2E, (byte) 0x61, (byte) 0x64, (byte) 0x6F, (byte) 0x62, // .adob
|
||||||
|
(byte) 0x65, (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, // e.com
|
||||||
|
(byte) 0x2F, (byte) 0x78, (byte) 0x61, (byte) 0x70, (byte) 0x2F, // /xap/
|
||||||
|
(byte) 0x31, (byte) 0x2E, (byte) 0x30, (byte) 0x2F, (byte) 0x00}; // 1.0/\0
|
||||||
|
|
||||||
|
public static byte[] markExtendedXMP = { //Length: 35
|
||||||
|
(byte) 0x68, (byte) 0x74, (byte) 0x74, (byte) 0x70, // http
|
||||||
|
(byte) 0x3A, (byte) 0x2F, (byte) 0x2F, (byte) 0x6E, (byte) 0x73, // ://ns
|
||||||
|
(byte) 0x2E, (byte) 0x61, (byte) 0x64, (byte) 0x6F, (byte) 0x62, // .adob
|
||||||
|
(byte) 0x65, (byte) 0x2E, (byte) 0x63, (byte) 0x6F, (byte) 0x6D, // e.com
|
||||||
|
(byte) 0x2F, (byte) 0x78, (byte) 0x6D, (byte) 0x70, (byte) 0x2F, // /xmp/
|
||||||
|
(byte) 0x65, (byte) 0x78, (byte) 0x74, (byte) 0x65, (byte) 0x6e, // exten
|
||||||
|
(byte) 0x73, (byte) 0x69, (byte) 0x6f, (byte) 0x6e, (byte) 0x2f, // sion/
|
||||||
|
(byte) 0x00}; // \0
|
||||||
|
|
||||||
|
public static byte[] keyHasExtendedXMP = { //xmpNote:HasExtendedXMP
|
||||||
|
0x78,0x6d,0x70,0x4e,0x6f,0x74,0x65,0x3a,0x48,0x61,0x73,0x45,0x78,0x74,0x65,0x6e,0x64,0x65,0x64,0x58,0x4d,0x50
|
||||||
|
};
|
||||||
|
|
||||||
|
public static byte[] keyGDepthData = {0x47,0x44,0x65,0x70,0x74,0x68,0x3a,0x44,0x61,0x74,0x61}; //GDepth:Data
|
||||||
|
|
||||||
|
public static byte[] keyGImageData = {0x47,0x49,0x6d,0x61,0x67,0x65,0x3a,0x44,0x61,0x74,0x61}; //GImage:Data
|
||||||
|
}
|
51
src/DepthMapNeedle.java
Normal file
51
src/DepthMapNeedle.java
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
|
||||||
|
public class DepthMapNeedle
|
||||||
|
{
|
||||||
|
public static void main(String[] args)
|
||||||
|
{
|
||||||
|
if(args.length==0 || args[0].equals("-h")) help();
|
||||||
|
if(args.length >= 2 && args[0].equals("-e"))
|
||||||
|
{
|
||||||
|
for(int i=1; i<args.length; i++)
|
||||||
|
{
|
||||||
|
JPEG image = new JPEG(args[i]);
|
||||||
|
image.exportDepthMap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(args.length >=2 && args[0].equals("-s"))
|
||||||
|
{
|
||||||
|
for(int i=1; i<args.length; i++)
|
||||||
|
{
|
||||||
|
JPEG image = new JPEG(args[i]);
|
||||||
|
image.exportSourceImage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(args.length >= 3 && args[0].equals("-i"))
|
||||||
|
{
|
||||||
|
String depthmap = args[1];
|
||||||
|
for(int i=2; i<args.length; i++)
|
||||||
|
{
|
||||||
|
JPEG image = new JPEG(args[i]);
|
||||||
|
image.injectDepthMap(depthmap);
|
||||||
|
image.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
103
src/Gui.java
Normal file
103
src/Gui.java
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import java.awt.BorderLayout;
|
||||||
|
import java.awt.Dimension;
|
||||||
|
import java.awt.FlowLayout;
|
||||||
|
import java.awt.event.ActionEvent;
|
||||||
|
import java.awt.event.ActionListener;
|
||||||
|
|
||||||
|
import javax.swing.BoxLayout;
|
||||||
|
import javax.swing.JButton;
|
||||||
|
import javax.swing.JFrame;
|
||||||
|
import javax.swing.JPanel;
|
||||||
|
import javax.swing.JScrollPane;
|
||||||
|
import javax.swing.JTable;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import javax.swing.UnsupportedLookAndFeelException;
|
||||||
|
import javax.swing.table.DefaultTableModel;
|
||||||
|
|
||||||
|
|
||||||
|
public class Gui extends JFrame
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static final long serialVersionUID=1L;
|
||||||
|
JTable images;
|
||||||
|
JButton execute;
|
||||||
|
JButton close;
|
||||||
|
|
||||||
|
public Gui()
|
||||||
|
{
|
||||||
|
super();
|
||||||
|
this.setTitle("DepthMapExtractor");
|
||||||
|
this.setVisible(true);
|
||||||
|
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
|
this.setSize(new Dimension(600,400));
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {}
|
||||||
|
JPanel content = new JPanel(new BorderLayout());
|
||||||
|
this.add(content);
|
||||||
|
DefaultTableModel model = new DefaultTableModel();
|
||||||
|
model.addColumn("filename");
|
||||||
|
this.images = new JTable(model);
|
||||||
|
JButton addBtn = new JButton("+");
|
||||||
|
addBtn.addActionListener(new ActionListener(){
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent arg0)
|
||||||
|
{
|
||||||
|
//TODO: AddFilesDialog
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JButton remBtn = new JButton("-");
|
||||||
|
remBtn.addActionListener(new ActionListener(){
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent arg0)
|
||||||
|
{
|
||||||
|
//TODO: Remove selected files from table
|
||||||
|
}
|
||||||
|
});
|
||||||
|
JPanel tablePanel = new JPanel(new FlowLayout());
|
||||||
|
tablePanel.add(new JScrollPane(images));
|
||||||
|
JPanel tableButtons = new JPanel();
|
||||||
|
tableButtons.setLayout(new BoxLayout(tableButtons,BoxLayout.Y_AXIS));
|
||||||
|
tableButtons.add(addBtn);
|
||||||
|
tableButtons.add(remBtn);
|
||||||
|
tablePanel.add(tableButtons);
|
||||||
|
content.add(tablePanel, BorderLayout.NORTH);
|
||||||
|
|
||||||
|
execute = new JButton("Execute");
|
||||||
|
execute.addActionListener(new ActionListener(){
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent arg0)
|
||||||
|
{
|
||||||
|
//TODO: Actions
|
||||||
|
}
|
||||||
|
});
|
||||||
|
close = new JButton("Close");
|
||||||
|
close.addActionListener(new ActionListener(){
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent arg0)
|
||||||
|
{
|
||||||
|
dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
JPanel buttons = new JPanel(new FlowLayout());
|
||||||
|
buttons.add(close); buttons.add(execute);
|
||||||
|
content.add(buttons,BorderLayout.SOUTH);
|
||||||
|
this.pack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void executeExtraction()
|
||||||
|
{
|
||||||
|
DefaultTableModel model = (DefaultTableModel) images.getModel();
|
||||||
|
for(int i=0; i<model.getRowCount(); i++)
|
||||||
|
{
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
String filepath = (String) model.getValueAt(i, 0);
|
||||||
|
//DepthMapExtractor dme = new DepthMapExtractor(filepath);
|
||||||
|
//dme.extractDepthMap();
|
||||||
|
//TODO: check checkboxes etc.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/HexUtil.java
Normal file
48
src/HexUtil.java
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class HexUtil
|
||||||
|
{
|
||||||
|
public static void printHex(byte[] bytes)
|
||||||
|
{
|
||||||
|
for(byte b : bytes) System.out.print(byteToHex(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String byteToHex(byte data) {
|
||||||
|
|
||||||
|
StringBuffer buf = new StringBuffer();
|
||||||
|
buf.append(toHexChar((data >>> 4) & 0x0F));
|
||||||
|
buf.append(toHexChar(data & 0x0F));
|
||||||
|
|
||||||
|
return buf.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static char toHexChar(int i) {
|
||||||
|
if ((0 <= i) && (i <= 9)) {
|
||||||
|
return (char) ('0' + i);
|
||||||
|
} else {
|
||||||
|
return (char) ('a' + (i - 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] generateMD5(byte[] data)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] md5 = MessageDigest.getInstance("MD5").digest(data);
|
||||||
|
String m="";
|
||||||
|
for(int i=0; i<md5.length; i++)
|
||||||
|
{
|
||||||
|
m = m + byteToHex(md5[i]).toUpperCase();
|
||||||
|
}
|
||||||
|
return m.getBytes();
|
||||||
|
} catch (NoSuchAlgorithmException e)
|
||||||
|
{
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
src/IO.java
Normal file
52
src/IO.java
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
|
public class IO
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Read file from disk to byte array
|
||||||
|
* @param file path to file
|
||||||
|
* @return byte array
|
||||||
|
*/
|
||||||
|
public static byte[] read(File file) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
InputStream is = new FileInputStream(file);
|
||||||
|
byte[] b = new byte[(int) file.length()];
|
||||||
|
is.read(b);
|
||||||
|
is.close();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
System.err.println("Couldn't read file "+file.getAbsolutePath());
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write byte array to file on disk
|
||||||
|
* @param b byte array
|
||||||
|
* @param filepath path to outputfile
|
||||||
|
*/
|
||||||
|
public static void write(byte[] b, String filepath)
|
||||||
|
{
|
||||||
|
FileOutputStream fos;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fos=new FileOutputStream(filepath);
|
||||||
|
fos.write(b);
|
||||||
|
fos.close();
|
||||||
|
} catch (Exception e)
|
||||||
|
{
|
||||||
|
System.err.println("Couldn't write file "+filepath);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
190
src/JPEG.java
Normal file
190
src/JPEG.java
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
public class JPEG extends Const
|
||||||
|
{
|
||||||
|
private String filename;
|
||||||
|
private byte[] rawData;
|
||||||
|
private byte[] exif;
|
||||||
|
private byte[] xmpSta;
|
||||||
|
private byte[] xmpExt;
|
||||||
|
private byte[] tail;
|
||||||
|
|
||||||
|
public JPEG(byte[] rawData)
|
||||||
|
{
|
||||||
|
this.init(rawData, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JPEG(String filename)
|
||||||
|
{
|
||||||
|
this.init(IO.read(new File(filename)), filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disassemble()
|
||||||
|
{
|
||||||
|
exif = this.getEXIFBlock();
|
||||||
|
xmpSta = this.getStandardXMPBlockContent();
|
||||||
|
xmpExt = this.getExtendedXMPContent();
|
||||||
|
tail = this.getImageTail();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportDepthMap()
|
||||||
|
{
|
||||||
|
String out = filename;
|
||||||
|
if(out.endsWith(".jpg") || out.endsWith(".JPG")) out = out.substring(0, out.length()-4);
|
||||||
|
this.exportDepthMap(out+"_d.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportDepthMap(String file)
|
||||||
|
{
|
||||||
|
byte[] b64 = ArrayUtils.unsign(extractDepthMap());
|
||||||
|
byte[] depth = Base64.getDecoder().decode(b64);
|
||||||
|
IO.write(depth, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportSourceImage()
|
||||||
|
{
|
||||||
|
String out = filename;
|
||||||
|
if(out.endsWith(".jpg") || out.endsWith(".JPG")) out = out.substring(0, out.length()-4);
|
||||||
|
this.exportSourceImage(out+"_s.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void exportSourceImage(String file)
|
||||||
|
{
|
||||||
|
byte[] b64 = ArrayUtils.unsign(extractSourceImage());
|
||||||
|
byte[] src = Base64.getDecoder().decode(b64);
|
||||||
|
IO.write(src, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] extractDepthMap()
|
||||||
|
{
|
||||||
|
return JPEGUtils.extractDepthMap(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] extractSourceImage()
|
||||||
|
{
|
||||||
|
return JPEGUtils.extractSourceImage(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] getBoundaries()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getBoundaries(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getExif()
|
||||||
|
{
|
||||||
|
return exif;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getEXIFBlock()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getEXIFBlock(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getExtendedXMPContent()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getExtendedXMPBlockContent(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getFilename()
|
||||||
|
{
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getImageTail()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getImageTail(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getMetadata()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getMetadata(rawData);
|
||||||
|
}
|
||||||
|
public byte[] getRawData()
|
||||||
|
{
|
||||||
|
return this.rawData;
|
||||||
|
}
|
||||||
|
public byte[] getStandardXMPBlockContent()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getStandardXMPBlockContent(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getTail()
|
||||||
|
{
|
||||||
|
return tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getXMPBlocksContent()
|
||||||
|
{
|
||||||
|
return JPEGUtils.getXMPBlocksContent(rawData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getXmpExt()
|
||||||
|
{
|
||||||
|
return xmpExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getXmpSta()
|
||||||
|
{
|
||||||
|
return xmpSta;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init(byte[] raw, String filename)
|
||||||
|
{
|
||||||
|
this.rawData = raw;
|
||||||
|
this.filename = filename;
|
||||||
|
this.disassemble();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void injectDepthMap(String filename)
|
||||||
|
{
|
||||||
|
byte[] depth = Base64.getEncoder().encode(IO.read(new File(filename)));
|
||||||
|
xmpExt = JPEGUtils.replace(xmpExt, keyGDepthData, depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] reassemble()
|
||||||
|
{
|
||||||
|
byte[] md5 = HexUtil.generateMD5(xmpExt);
|
||||||
|
|
||||||
|
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 = JPEGUtils.replace(out, keyHasExtendedXMP, md5);
|
||||||
|
this.rawData = out;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save()
|
||||||
|
{
|
||||||
|
this.save(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(String file)
|
||||||
|
{
|
||||||
|
IO.write(this.reassemble(), file);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExif(byte[] exif)
|
||||||
|
{
|
||||||
|
this.exif=exif;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTail(byte[] tail)
|
||||||
|
{
|
||||||
|
this.tail=tail;
|
||||||
|
}
|
||||||
|
public void setXmpExt(byte[] xmpExt)
|
||||||
|
{
|
||||||
|
this.xmpExt=xmpExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setXmpSta(byte[] xmpSta)
|
||||||
|
{
|
||||||
|
this.xmpSta=xmpSta;
|
||||||
|
}
|
||||||
|
}
|
333
src/JPEGUtils.java
Normal file
333
src/JPEGUtils.java
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
public class JPEGUtils extends Const
|
||||||
|
{
|
||||||
|
|
||||||
|
public static int blockLength(int boundaryPos, byte[] data)
|
||||||
|
{
|
||||||
|
if(!JPEGUtils.isAPP1Marker(data,boundaryPos))
|
||||||
|
{
|
||||||
|
System.err.println("JPEGUtils blockLength: Block is no APP1 Block!");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int o;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
o = 256*(data[boundaryPos+2]&0xFF) + (data[boundaryPos+3]&0xFF);
|
||||||
|
}
|
||||||
|
catch(ArrayIndexOutOfBoundsException e)
|
||||||
|
{
|
||||||
|
System.err.println("JPEGUtils blockLength got ArrayIndexOutOfBoundsException:");
|
||||||
|
e.printStackTrace();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
public static byte[] decorateBlock(byte[] data, String type)
|
||||||
|
{
|
||||||
|
if(type.equals(EXIF))
|
||||||
|
{
|
||||||
|
data = ArrayUtils.concatenate(markEXIF, data);
|
||||||
|
byte[] pre = ArrayUtils.concatenate(markAPP1, genLen(data.length+2));
|
||||||
|
return ArrayUtils.concatenate(pre, 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);
|
||||||
|
}
|
||||||
|
else if(type.equals(EXTENDEDXMP))
|
||||||
|
{
|
||||||
|
byte[] out = new byte[0];
|
||||||
|
byte[] md5 = HexUtil.generateMD5(data);
|
||||||
|
int i=0;
|
||||||
|
int blockCount = data.length/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++;
|
||||||
|
}
|
||||||
|
//Rest
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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 for \""+new String(key) +"\": false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.err.println("JPEGUtils extract found start for \""+new String(key) +"\": false");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
public static byte[] genLen(int l)
|
||||||
|
{
|
||||||
|
byte[] o = new byte[2];
|
||||||
|
o[0] = (byte) (l/256);
|
||||||
|
o[1] = (byte) (l%256);
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return Arrays.copyOfRange(data, boundary, boundary+JPEGUtils.blockLength(boundary, data)+2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.blockLength(boundary,data)+2);
|
||||||
|
|
||||||
|
}
|
||||||
|
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.blockLength(boundary,data)+2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return all positions of "FF E1" in the byte[]
|
||||||
|
* @param data byte array
|
||||||
|
* @return array of positions
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the exif block of the jpg. This is usually the first block of data
|
||||||
|
* @param data
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
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 is null");
|
||||||
|
cont = ArrayUtils.concatenate(cont, part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cont;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
offset += bounds[bounds.length-1];
|
||||||
|
offset +=2;
|
||||||
|
out = Arrays.copyOfRange(data, offset, data.length);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
public static boolean isAPP1Marker(byte[] data, int offset)
|
||||||
|
{
|
||||||
|
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markAPP1, offset);
|
||||||
|
}
|
||||||
|
public static boolean isEXIFBlock(byte[] block)
|
||||||
|
{
|
||||||
|
return JPEGUtils.isEXIFMarker(block, 4);
|
||||||
|
}
|
||||||
|
public static boolean isEXIFMarker(byte[] data, int offset)
|
||||||
|
{
|
||||||
|
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markEXIF, offset);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Block is the block with FFE1 and the two following bytes
|
||||||
|
* @param block block
|
||||||
|
* @return true, if the block is extended xmp
|
||||||
|
*/
|
||||||
|
public static boolean isExtendedXMP(byte[] block)
|
||||||
|
{
|
||||||
|
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(block, markExtendedXMP, 4);
|
||||||
|
}
|
||||||
|
public static boolean isJPG(byte[] data)
|
||||||
|
{
|
||||||
|
return JPEGUtils.isJPG(data, 0);
|
||||||
|
}
|
||||||
|
public static boolean isJPG(byte[] data, int offset)
|
||||||
|
{
|
||||||
|
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(data, markJPG, offset);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Block is the block with FFE1 and the two following bytes
|
||||||
|
* @param block block
|
||||||
|
* @return true, if the block is standard xmp
|
||||||
|
*/
|
||||||
|
public static boolean isStandardXMP(byte[] block)
|
||||||
|
{
|
||||||
|
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(block, markStandardXMP, 4);
|
||||||
|
}
|
||||||
|
public static boolean isStandardXMP(byte[] block, int offset)
|
||||||
|
{
|
||||||
|
return ArrayUtils.arrayIsPartOfOtherArrayOnOffset(block, markStandardXMP, offset);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue