/*
* Copyright (c) 2008 Los Alamos National Security, LLC.
*
* Los Alamos National Laboratory
* Research Library
* Digital Library Research & Prototyping Team
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
package gov.lanl.adore.djatoka.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.List;
/**
* JPEG 2000 Metadata Parser
* @author Ryan Chute
*
*/
public class JP2ImageInfo implements JP2Markers {
private InputStream is;
private int currentDataLength;
private int currentMarker;
private ImageRecord ir;
private List<String> xmlDocs;
public JP2ImageInfo(File f) throws IOException {
this(new BufferedInputStream(new FileInputStream(f)));
ir.setImageFile(f.getAbsolutePath());
}
public JP2ImageInfo(InputStream is) throws IOException {
this.is = is;
ir = new ImageRecord();
setImageInfo();
}
/**
* Gets a populated ImageRecords for the initialized image
* @return a populated ImageRecord
*/
public ImageRecord getImageRecord() {
return ir;
}
/**
* Gets a list of xml docs contained in the JPEG 2000 header
*
*/
public String[] getXmlDocs() {
if (xmlDocs != null)
return xmlDocs.toArray(new String[xmlDocs.size()]);
else
return null;
}
private void setImageInfo() throws IOException {
try {
currentDataLength = read(4);
if (currentDataLength == MARKER_JP_LEN) {
currentMarker = read(4);
if (MARKER_JP != currentMarker) {
throw new IOException("Expected JP Marker");
}
if (MARKER_JP_SIG != read(4)) {
throw new IOException("Invalid JP Marker");
}
nextHeader();
if (MARKER_FTYP != currentMarker) {
throw new IOException("FTYP Marker not found");
}
skip(currentDataLength - 8);
nextHeader();
boolean done = false;
do {
if (MARKER_JP2H == currentMarker) {
nextHeader();
} else if (MARKER_IHDR == currentMarker) {
setIHDR();
nextHeader();
} else if (MARKER_COLR == currentMarker) {
setCOLR();
nextHeader();
} else if (MARKER_RES_BOX == currentMarker) {
setResBox();
nextHeader();
} else if (MARKER_JP2C == currentMarker) {
setJP2C();
done = true;
} else if (MARKER_XML == currentMarker) {
addXmlDoc(getXML());
nextHeader();
} else if (MARKER_SOC == currentMarker) {
done = true;
} else {
skip(currentDataLength - 8);
nextHeader();
}
} while (!done);
int compLayers = countCompLayers(readBytes(is.available()));
ir.setCompositingLayerCount(compLayers);
} else {
throw new IOException("Invalid Jpeg2000 file");
}
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {}
is = null;
}
}
}
/**
* Add a doc to the list of xml docs contained in the JPEG 2000 header
* @param docs
*/
private void addXmlDoc(String doc) {
if (xmlDocs == null)
xmlDocs = new LinkedList<String>();
this.xmlDocs.add(doc);
}
private void nextHeader() throws IOException {
currentDataLength = read(4);
currentMarker = read(4);
if (currentDataLength == 1) {
// Process SuperBox
if (read(4) != 0) {
throw new IOException("Box length too large");
}
currentDataLength = read(4);
if (currentDataLength == 0)
throw new IOException("Invalid box size");
} else if (currentDataLength <= 0 && currentMarker != MARKER_JP2C) {
throw new IOException("Invalid box size");
}
}
private int read(int n) throws IOException {
int c = 0;
for (int i = n - 1; i >= 0; i--) {
c |= (0xff & is.read()) << (8 * i);
}
return c;
}
private void skip(int n) throws IOException {
long i;
while (n > 0) {
i = is.skip(n);
if (i <= 0)
break;
n -= i;
}
}
private byte[] readBytes(int n) throws IOException {
byte[] b = new byte[n];
is.read(b);
return b;
}
private static int countCompLayers(byte[] data) {
byte[] pattern = MARKER_JPLH_BIN;
int cnt = 1;
int j = 1;
if (data.length == 0)
return 0;
for (int i = 0; i < data.length; i++) {
if (pattern[j] == data[i]) {
j++;
} else {
j = 1;
}
if (j == pattern.length) {
cnt++;
j = 1;
}
}
return cnt;
}
private void setIHDR() throws IOException {
int scaledHeight = read(4);
ir.setHeight(scaledHeight);
int scaledWidth = read(4);
ir.setWidth(scaledWidth);
int components = read(2);
ir.setNumChannels(components);
int bitDepth = read(1);
ir.setBitDepth((bitDepth == 7) ? bitDepth + 1 : bitDepth);
int compression = read(1);
int unknownColor = read(1);
int intelProp = read(1);
}
private void setCOLR() throws IOException {
int method = read(1);
int precedence = read(1);
int approximation = read(1);
if (method == 2) {
int proData = read(currentDataLength - 3);
// ICC Profile Needs to be set
} else {
int c = read(4);
}
}
private void setResBox() throws IOException {
int vn = read(3);
int vd = read(3);
int hn = read(3);
int hd = read(3);
int ve = read(3);
int he = read(3);
}
private String getXML() throws IOException {
// Subtract XML Marker, Length Value, XML Flag
byte[] xml = readBytes(currentDataLength - 16);
if (xml != null)
return new String(xml);
else
return null;
}
private void setJP2C() throws IOException {
int soc = read(2); // SOC
boolean hend = false;
while (!hend) {
int h = read(2);
if (h == MARKER_SIZ) { // SIZ
int lsiz = read(2); // Length of SIZ
int rsiz = read(2);
int xsiz = read(4);
int ysiz = read(4);
int xosiz = read(4);
int yosiz = read(4);
int xtsiz = read(4);
int ytsiz = read(4);
int xtosize = read(4);
int ytosize = read(4);
int csiz = read(2);
int ssiz = read(1);
int xrsiz = read(1);
int yrsiz = read(1);
int a0 = read(2);
int a1 = read(2);
int a2 = read(2);
} else if (h == MARKER_COD) { // COD
int lcod = read(2); // Length of COD
int scod = read(1);
int sgcod_porder = read(1); // Progression Order
int sgcod_layers = read(2); // Number of layers
ir.setQualityLayers(sgcod_layers);
int sgcod_ctrans = read(1); // Component Transformation Type
int sgcod_levels = read(1); // Number of levels
ir.setDWTLevels(sgcod_levels);
int djatokaLevels = ImageProcessingUtils.getLevelCount(ir.getWidth() , ir.getHeight());
ir.setLevels((djatokaLevels > sgcod_levels) ? sgcod_levels : djatokaLevels);
int sgcod_cb_width = read(1); // code-block width
int sgcod_cb_height = read(1); // code-block height
int sgcod_cb_style = read(1); // code-block style
int sgcod_wavelet = read(1); // wavelet type (9:7 && 5:3)
hend = true;
} else {
throw new IOException("Expecting MARKER_COD or MARKER_SIZ in header");
}
}
}
public static void main(String[] args) throws Exception {
JP2ImageInfo jp2 = new JP2ImageInfo(new File(args[0]));
ImageRecord ir = jp2.getImageRecord();
System.out.println(ir.toString());
}
}