/*
* Copyright 2006-2017 ICEsoft Technologies Canada Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS
* IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package org.icepdf.core.pobjects.filters;
import org.icepdf.core.io.BitStream;
import org.icepdf.core.io.ZeroPaddedInputStream;
import org.icepdf.core.pobjects.ImageStream;
import org.icepdf.core.pobjects.Stream;
import org.icepdf.core.util.Library;
import org.icepdf.core.util.Utils;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import java.awt.*;
import java.awt.image.*;
import java.awt.image.renderable.ParameterBlock;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Many facsimile and document imaging file formats support a form of lossless
* data compression often described as CCITT encoding. The CCITT (International
* Telegraph and Telephone Consultative Committee) is a standards organization
* that has developed a series of communications protocols for the facsimile
* transmission of black-and-white images over telephone lines and data networks.
* These protocols are known officially as the CCITT T.4 and T.6 standards but
* are more commonly referred to as CCITT Group 3 and Group 4 compression,
* respectively.
* <p>
* The CCITT actually defines three algorithms for the encoding of bi-level image data:
* Group 3 One-Dimensional (G31D)
* Group 3 Two-Dimensional (G32D) - not implemented
* Group 4 Two-Dimensional (G42D)
*/
public class CCITTFax {
private static final Logger logger =
Logger.getLogger(CCITTFax.class.toString());
// white codes
static final String[] _twcodes = {
"00110101", "000111", "0111", "1000", "1011", "1100", "1110", "1111",
"10011", "10100", "00111", "01000", "001000", "000011", "110100",
"110101", "101010", "101011", "0100111", "0001100", "0001000",
"0010111", "0000011", "0000100", "0101000", "0101011", "0010011",
"0100100", "0011000", "00000010", "00000011", "00011010", "00011011",
"00010010", "00010011", "00010100", "00010101", "00010110",
"00010111", "00101000", "00101001", "00101010", "00101011",
"00101100", "00101101", "00000100", "00000101", "00001010",
"00001011", "01010010", "01010011", "01010100", "01010101",
"00100100", "00100101", "01011000", "01011001", "01011010",
"01011011", "01001010", "01001011", "00110010", "00110011",
"00110100"
};
// wite codes
static final String[] _mwcodes = {
"11011", "10010", "010111", "0110111", "00110110", "00110111", "01100100",
"01100101", "01101000", "01100111", "011001100", "011001101",
"011010010", "011010011", "011010100", "011010101", "011010110",
"011010111", "011011000", "011011001", "011011010", "011011011",
"010011000", "010011001", "010011010", "011000", "010011011"
};
// black codes
static final String[] _tbcodes = {
"0000110111", "010", "11", "10", "011", "0011", "0010", "00011", "000101",
"000100", "0000100", "0000101", "0000111", "00000100", "00000111",
"000011000", "0000010111", "0000011000", "0000001000", "00001100111",
"00001101000", "00001101100", "00000110111", "00000101000",
"00000010111", "00000011000", "000011001010", "000011001011",
"000011001100", "000011001101", "000001101000", "000001101001",
"000001101010", "000001101011", "000011010010", "000011010011",
"000011010100", "000011010101", "000011010110", "000011010111",
"000001101100", "000001101101", "000011011010", "000011011011",
"000001010100", "000001010101", "000001010110", "000001010111",
"000001100100", "000001100101", "000001010010", "000001010011",
"000000100100", "000000110111", "000000111000", "000000100111",
"000000101000", "000001011000", "000001011001", "000000101011",
"000000101100", "000001011010", "000001100110", "000001100111"
};
// black codes
static final String[] _mbcodes = {
"0000001111", "000011001000", "000011001001", "000001011011", "000000110011",
"000000110100", "000000110101", "0000001101100", "0000001101101",
"0000001001010", "0000001001011", "0000001001100", "0000001001101",
"0000001110010", "0000001110011", "0000001110100", "0000001110101",
"0000001110110", "0000001110111", "0000001010010", "0000001010011",
"0000001010100", "0000001010101", "0000001011010", "0000001011011",
"0000001100100", "0000001100101"
};
static final String[] _extmcodes = {
"00000001000", "00000001100", "00000001101", "000000010010", "000000010011",
"000000010100", "000000010101", "000000010110", "000000010111",
"000000011100", "000000011101", "000000011110", "000000011111"
};
// Mode command binary values
static final String[] _modecodes = {
"0001", // P - Pass Mode
"001", // H - Horizontal Mode, when neither P or V Modes
"1", // V0 - Vertical Mode
"011", // VR1 - Vertical Mode, one pixel to the right
"000011", // VR2 - Vertical Mode, two pixel to the right
"0000011", // VR3 - Vertical Mode, three pixel to the right
"010", // VL1 - Vertical Mode, one pixel to the left
"000010", // VL2 - Vertical Mode, two pixel to the left
"0000010", // VL3 - Vertical Mode, three pixel to the left
"0000001111", // EXT2D - Extension, 2D
"000000001111", // EXT1D - Extension, 1D
"000000000001" // EOL
};
private static class Code {
private long value;
private int length;
private int tablePosition;
public Code() {
value = 0L;
length = 0;
}
public Code(String strValue, int tablePosition) {
value = 0L;
length = 0;
this.tablePosition = tablePosition;
for (int i = 0; i < strValue.length(); i++)
append(strValue.charAt(i) == '1');
}
public final void append(boolean bit) {
// This is effectively similar to the old String code,
// which kept the extra bits, but would then not match
// any of the table entries
if (bit) {
if (length <= 63) {
long mask = (1L << length);
value |= mask;
}
}
length++;
}
public final boolean equals(Object ob) {
if (ob instanceof Code) {
Code c = (Code) ob;
return (value == c.value && length == c.length);
}
return false;
}
public final void reset() {
value = 0L;
length = 0;
}
public final int getLength() {
return length;
}
public final int getTablePosition() {
return tablePosition;
}
}
static final Code[][] twcodes = convertStringArrayToCodeArray2D(_twcodes);
static final Code[][] mwcodes = convertStringArrayToCodeArray2D(_mwcodes);
static final Code[][] tbcodes = convertStringArrayToCodeArray2D(_tbcodes);
static final Code[][] mbcodes = convertStringArrayToCodeArray2D(_mbcodes);
static final Code[][] extmcodes = convertStringArrayToCodeArray2D(_extmcodes);
static final Code[][] modecodes = convertStringArrayToCodeArray2D(_modecodes);
private static Code[][] convertStringArrayToCodeArray2D(String[] strArray) {
int len = strArray.length;
// Make histogram of sizes
int[] codeLengths = new int[64];
for (String aStrArray : strArray) {
int entryLength = aStrArray.length();
codeLengths[entryLength]++;
}
// Make a 2d array of Code objects, where the first index is for
// the length of the Code, and the second index differentiates
// between all Code objects with that length
// In this way, we separate all Code objects by their length,
// and can thus reduce the search space
// In theory, we could then sort each sublist, and do a binary search...
int largestLength = codeLengths.length - 1;
while (largestLength > 0 && codeLengths[largestLength] == 0)
largestLength--;
Code[][] codeArray = new Code[largestLength + 1][];
for (int i = 0; i < codeArray.length; i++)
codeArray[i] = new Code[codeLengths[i]];
for (int i = 0; i < len; i++) {
int entryLength = strArray[i].length();
Code[] entries = codeArray[entryLength];
for (int j = 0; j < entries.length; j++) {
if (entries[j] == null) {
entries[j] = new Code(strArray[i], i);
break;
}
}
}
return codeArray;
}
private static int findPositionInTable(Code lookFor, Code[][] lookIn) {
int lookForIndex = lookFor.getLength();
if (lookForIndex >= lookIn.length)
return -1;
Code[] lookInWithSameLength = lookIn[lookForIndex];
if (lookInWithSameLength == null)
return -1;
for (Code potentialMatch : lookInWithSameLength) {
if (lookFor.equals(potentialMatch))
return potentialMatch.getTablePosition();
}
return -1;
}
// Black and white colour bit values.
static int black = 0;
static int white = 1;
// Never actually used.
// class FaxCode {
// FaxCode zero;
// FaxCode one;
// boolean leaf;
// int tipo;
// int len;
// }
private static final short TIFF_COMPRESSION_NONE_default = 1;
private static final short TIFF_COMPRESSION_GROUP3_1D = 2;
private static final short TIFF_COMPRESSION_GROUP3_2D = 3;
private static final short TIFF_COMPRESSION_GROUP4 = 4;
private static final String[] TIFF_COMPRESSION_NAMES = new String[]{
"",
"TIFF_COMPRESSION_NONE_default",
"TIFF_COMPRESSION_GROUP3_1D",
"TIFF_COMPRESSION_GROUP3_2D",
"TIFF_COMPRESSION_GROUP4"
};
private static final short TIFF_PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO_default = 0;
private static final short TIFF_PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO = 1;
private static boolean USE_JAI_IMAGE_LIBRARY = false;
private static Method jaiCreate = null;
private static Method ssWrapInputStream = null;
private static Method roGetAsBufferedImage = null;
static {
try {
Class<?> jaiClass = Class.forName("javax.media.jai.JAI");
jaiCreate = jaiClass.getMethod("create", String.class, ParameterBlock.class);
Class<?> ssClass = Class.forName("com.sun.media.jai.codec.SeekableStream");
ssWrapInputStream = ssClass.getMethod("wrapInputStream", InputStream.class, Boolean.TYPE);
Class<?> roClass = Class.forName("javax.media.jai.RenderedOp");
roGetAsBufferedImage = roClass.getMethod("getAsBufferedImage");
USE_JAI_IMAGE_LIBRARY = true;
} catch (Exception e) {
logger.info("javax.media.jai.JAI could not bef found on the class path");
}
if (logger.isLoggable(Level.FINER)) {
Iterator<ImageReader> iter = ImageIO.getImageReadersByFormatName("TIFF");
ImageReader reader;
while (iter.hasNext()) {
reader = iter.next();
logger.finer("CCITTFaxDecode Image reader: " + reader);
}
}
}
/**
* Map bitstream values to tw and mw codes.
*
* @param inb bit stream containing the CCITT data
* @throws java.io.IOException
*/
static int findWhite(BitStream inb, Code code) throws IOException {
return findTone(inb, code, twcodes, mwcodes);
}
/**
* Finds the next black occruence in the stream
*
* @throws java.io.IOException
*/
static int findBlack(BitStream inb, Code code) throws IOException {
return findTone(inb, code, tbcodes, mbcodes);
}
static int findTone(BitStream inb, Code code, Code[][] tCodes, Code[][] mCodes) throws IOException {
code.reset();
while (!inb.atEndOfFile()) {
int i = inb.getBits(1);
code.append(i != 0);
int j;
j = findPositionInTable(code, tCodes);
if (j >= 0) {
//System.err.println("BINGO! tb "+_tbcodes[j]+" "+j);
return j;
}
j = findPositionInTable(code, mCodes);
if (j >= 0) {
//System.err.println("BINGO! mb "+_mbcodes[j]+" "+(j+1)*64);
return (j + 1) * 64;
}
j = findPositionInTable(code, extmcodes);
if (j >= 0) {
//System.err.println("BINGO! extm "+_extmcodes[j]+" "+(1792+j*64));
return (1792 + j * 64);
}
}
inb.close();
//System.err.println("CODE ERROR! " + code);
return 0;
}
/**
* @throws java.io.IOException
*/
static void addRun(int x, G4State s, BitStream out) throws IOException {
s.runLength += x;
s.cur[s.curIndex++] = s.runLength;
s.a0 += x;
if (s.runLength > 0) {
// black/white color switch !s.white
out.putRunBits(s.white ? white : black, s.runLength);
}
out.close();
s.runLength = 0;
}
/**
* @throws java.io.IOException
*/
static int readmode(BitStream inb, Code code) throws IOException {
code.reset();
while (!inb.atEndOfFile()) {
int i = inb.getBits(1);
code.append(i != 0);
int j = findPositionInTable(code, modecodes);
if (j >= 0) {
return j;
}
}
inb.close();
return -1;
}
/**
*/
static void detectB1(G4State s) {
if (s.curIndex != 0) {
while (s.b1 <= s.a0 && s.b1 < s.width) {
int r = s.ref[s.refIndex] + s.ref[s.refIndex + 1];
if (r == 0)
s.b1 = s.width;
s.b1 += r;
if (s.refIndex + 2 < s.ref.length) {
s.refIndex += 2;
}
// else {
//System.out.println("ERROR in detectB1, refIndex=" + s.refIndex
// + ", ref.length=" + s.ref.length);
// }
}
}
}
/**
*/
static void decodePass(G4State s) {
detectB1(s);
s.b1 += s.ref[s.refIndex++];
s.runLength += s.b1 - s.a0;
s.a0 = s.b1;
s.b1 += s.ref[s.refIndex++];
}
/**
* @throws java.io.IOException
*/
static void decodeHorizontal(BitStream in, BitStream out, G4State s, Code code) throws IOException {
int rl;
do {
rl = s.white ? findWhite(in, code) : findBlack(in, code);
if (rl >= 0) {
if (rl < 64) {
addRun(rl + s.longrun, s, out);
s.white = !s.white;
s.longrun = 0;
} else {
s.longrun += rl;
}
} else {
addRun(rl, s, out);
}
} while (rl >= 64);
out.close();
}
/**
* @throws java.io.IOException
*/
static void resetRuns(BitStream outb, G4State state) throws IOException {
//System.err.println("EOL! "+state.a0);
state.white = true;
addRun(0, state, outb);
if (state.a0 != state.width) {
//System.out.println( (state.a0 < state.width ? "Premature EOL" : "Line length mismatch") );
while (state.a0 > state.width)
state.a0 -= state.cur[--state.curIndex];
if (state.a0 < state.width) {
if (state.a0 < 0)
state.a0 = 0;
if ((state.curIndex & 0x1) != 0)
addRun(0, state, outb);
addRun(state.width - state.a0, state, outb);
} else if (state.a0 > state.width) {
addRun(state.width, state, outb);
addRun(0, state, outb);
}
}
int tmp[] = state.ref;
state.ref = state.cur;
state.cur = tmp;
//now zero out extra spots for runs
for (int i = state.curIndex; i < state.width; i++)
state.ref[i] = 0;
for (int i = 0; i < state.width; i++)
state.cur[i] = 0;
state.runLength = 0;
state.a0 = 0;
state.b1 = state.ref[0];
state.refIndex = 1;
state.curIndex = 0;
outb.close();
}
/**
*/
public static void Group4Decode(InputStream in, OutputStream out, int width, boolean blackIs1) {
BitStream inb = new BitStream(in);
BitStream outb = new BitStream(out);
// assign default colour mapping
black = 0;
white = 1;
// apply blackIs1, which inverts the colour pallet
if (blackIs1) {
black = 1;
white = 0;
}
Code code = new Code();
try {
G4State graphicState = new G4State(width);
while (!inb.atEndOfFile()) {
int mode = readmode(inb, code);
switch (mode) {
case 0: // P
decodePass(graphicState);
continue;
case 1: // H
decodeHorizontal(inb, outb, graphicState, code);
decodeHorizontal(inb, outb, graphicState, code);
detectB1(graphicState);
break;
case 2: // V0
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0, graphicState, outb);
graphicState.white = !graphicState.white;
graphicState.b1 += graphicState.ref[graphicState.refIndex++];
break;
case 3: // VR1
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0 + 1, graphicState, outb);
graphicState.white = !graphicState.white;
graphicState.b1 += graphicState.ref[graphicState.refIndex++];
break;
case 4: // VR2
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0 + 2, graphicState, outb);
graphicState.white = !graphicState.white;
graphicState.b1 += graphicState.ref[graphicState.refIndex++];
break;
case 5: // VR3
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0 + 3, graphicState, outb);
graphicState.white = !graphicState.white;
graphicState.b1 += graphicState.ref[graphicState.refIndex++];
break;
case 6: // VL1
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0 - 1, graphicState, outb);
graphicState.white = !graphicState.white;
if (graphicState.refIndex > 0)
graphicState.b1 -= graphicState.ref[--graphicState.refIndex];
break;
case 7: // VL2
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0 - 2, graphicState, outb);
graphicState.white = !graphicState.white;
if (graphicState.refIndex > 0)
graphicState.b1 -= graphicState.ref[--graphicState.refIndex];
break;
case 8: // VL3
detectB1(graphicState);
addRun(graphicState.b1 - graphicState.a0 - 3, graphicState, outb);
graphicState.white = !graphicState.white;
if (graphicState.refIndex > 0)
graphicState.b1 -= graphicState.ref[--graphicState.refIndex];
break;
case 11: // EOL
resetRuns(outb, graphicState);
break;
default:
//System.err.println("UNK! "+mode);
}
if (graphicState.a0 >= graphicState.width) {
resetRuns(outb, graphicState);
}
}
// do a little memory clean up.
inb.close();
outb.close();
in.close();
// out.flush(); // need this for further proccessing
out.close();
} catch (Exception e) {
logger.log(Level.FINE, "Error decoding group4 CITTFax", e);
}
}
public static BufferedImage attemptDeriveBufferedImageFromBytes(
ImageStream stream, Library library, HashMap streamDictionary, Color fill) throws InvocationTargetException, IllegalAccessException {
if (!USE_JAI_IMAGE_LIBRARY)
return null;
boolean imageMask = stream.isImageMask();
List decodeArray = (List) library.getObject(streamDictionary, ImageStream.DECODE_KEY);
// get decode parameters from stream properties
HashMap decodeParmsDictionary = library.getDictionary(streamDictionary, ImageStream.DECODEPARMS_KEY);
boolean blackIs1 = stream.getBlackIs1(library, decodeParmsDictionary);
// double check for blackIs1 in the main dictionary.
if (!blackIs1 && ImageStream.CHECK_PARENT_BLACK_IS_1) {
blackIs1 = stream.getBlackIs1(library, streamDictionary);
}
float k = library.getFloat(decodeParmsDictionary, ImageStream.K_KEY);
short compression = TIFF_COMPRESSION_NONE_default;
if (k < 0) compression = TIFF_COMPRESSION_GROUP4;
else if (k > 0) compression = TIFF_COMPRESSION_GROUP3_2D;
else if (k == 0) compression = TIFF_COMPRESSION_GROUP3_1D;
boolean hasHeader;
InputStream input = stream.getDecodedByteArrayInputStream();
if (input == null)
return null;
input = new ZeroPaddedInputStream(input);
BufferedInputStream bufferedInput = new BufferedInputStream(input, 1024);
bufferedInput.mark(4);
try {
int hb1 = bufferedInput.read();
int hb2 = bufferedInput.read();
bufferedInput.reset();
if (hb1 < 0 || hb2 < 0) {
input.close();
return null;
}
hasHeader = ((hb1 == 0x4d && hb2 == 0x4d) || (hb1 == 0x49 && hb2 == 0x49));
} catch (IOException e) {
try {
input.close();
} catch (IOException ioe) {
// keep quiet
}
return null;
}
input = bufferedInput;
BufferedImage img;
byte[] fakeHeaderBytes;
if (!hasHeader) {
// Apparently if the stream dictionary contains all the necessary info about
// the TIFF data in the stream, then some encoders omit the standard
// TIFF header in the stream, which confuses some image decoders, like JAI,
// in which case we inject a TIFF header which is derived from the stream
// dictionary.
fakeHeaderBytes = new byte[]{
// TIFF Header
0x4d, 0x4d, // 00 : Big (sane) endian
0x00, 0x2a, // 02 : Magic 42
0x00, 0x00, 0x00, 0x08, // 04 : Offset to first IFD
// First IFD
0x00, 0x0c, // 08 : Num Directory Entries
// Directory Entries: ushort tag, ushort type, uint count, uint valueOrOffset
0x00, (byte) 0xfe, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 0a : NewSubfileType
0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 16 : ImageWidth
0x01, 0x01, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 22 : ImageLength
0x01, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, // 2E : BitsPerSample
0x01, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, // 3A : Compression
0x01, 0x06, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 46 : PhotometricInterpretation
0x01, 0x11, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xAE, // 52 : StripOffsets
0x01, 0x16, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 5E : RowsPerStrip
0x01, 0x17, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 6A : StripByteCounts
0x01, 0x1A, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0x9E, // 76 : XResolution
0x01, 0x1B, 0x00, 0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0xA6, // 82 : YResolution
0x01, 0x28, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // 8E : ResolutionUnit
0x00, 0x00, 0x00, 0x00, // 9A : Next IFD
// Values from IFD, which don't fit in value field
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 9E : XResolution RATIONAL value
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01}; // A6 : YResolution RATIONAL value
// AE : Begin data
// Have to fill in values for: ImageWidth, ImageLength, BitsPerSample, Compression,
// PhotometricIntrerpretation, RowsPerStrip, StripByteCounts
boolean pdfStatesBlackAndWhite = false;
if (blackIs1) {
pdfStatesBlackAndWhite = true;
}
int width = library.getInt(streamDictionary, ImageStream.WIDTH_KEY);
int height = library.getInt(streamDictionary, ImageStream.HEIGHT_KEY);
Object columnsObj = library.getObject(decodeParmsDictionary, ImageStream.COLUMNS_KEY);
if (columnsObj != null && columnsObj instanceof Number) {
int columns = ((Number) columnsObj).intValue();
if (columns > width)
width = columns;
}
Utils.setIntIntoByteArrayBE(width, fakeHeaderBytes, 0x1E); // ImageWidth
Utils.setIntIntoByteArrayBE(height, fakeHeaderBytes, 0x2A); // ImageLength
Object bitsPerComponent = // BitsPerSample
library.getObject(streamDictionary, ImageStream.BITSPERCOMPONENT_KEY);
if (bitsPerComponent != null && bitsPerComponent instanceof Number) {
Utils.setShortIntoByteArrayBE(((Number) bitsPerComponent).shortValue(), fakeHeaderBytes, 0x36);
}
Utils.setShortIntoByteArrayBE(compression, fakeHeaderBytes, 0x42);
short photometricInterpretation = TIFF_PHOTOMETRIC_INTERPRETATION_WHITE_IS_ZERO_default;
// PDF has default BlackIs1=false ==> White=1, Black=0
// TIFF has default PhotometricInterpretation=0 ==> White=0, Black=1
// So, if PDF doesn't state what black and white are, then use TIFF's default
if (pdfStatesBlackAndWhite) {
if (!blackIs1)
photometricInterpretation = TIFF_PHOTOMETRIC_INTERPRETATION_BLACK_IS_ZERO;
}
Utils.setShortIntoByteArrayBE( // PhotometricInterpretation
photometricInterpretation, fakeHeaderBytes, 0x4E);
Utils.setIntIntoByteArrayBE(height, fakeHeaderBytes, 0x66); // RowsPerStrip
int lengthOfCompressedData = Integer.MAX_VALUE - 1; // StripByteCounts
Object lengthValue = library.getObject(streamDictionary, Stream.LENGTH_KEY);
if (lengthValue != null && lengthValue instanceof Number)
lengthOfCompressedData = ((Number) lengthValue).intValue();
else {
// JAI's SeekableStream pukes if we give a number too large
int approxLen = width * height;
if (approxLen > 0)
lengthOfCompressedData = approxLen;
}
Utils.setIntIntoByteArrayBE(lengthOfCompressedData, fakeHeaderBytes, 0x72);
ByteArrayInputStream fakeHeaderBytesIn = new ByteArrayInputStream(fakeHeaderBytes);
org.icepdf.core.io.SequenceInputStream sin = new org.icepdf.core.io.SequenceInputStream(fakeHeaderBytesIn, input);
img = deriveBufferedImageFromTIFFBytes(sin, library, lengthOfCompressedData, width, height, compression);
if (img == null) {
for (int i = 1; i <= 4; i++) { // Try the three other types of compression (1, 2, 3, 4)
compression++;
// We don't try the default uncompressed format, because it sometimes
// returns a blank image, which we don't want. If JAI fails, we
// want it to return null, so that the fallback code can have a try
if (compression > TIFF_COMPRESSION_GROUP4)
compression = TIFF_COMPRESSION_GROUP3_1D;
Utils.setShortIntoByteArrayBE(compression, fakeHeaderBytes, 0x42);
input = stream.getDecodedByteArrayInputStream();
if (input == null)
return null;
input = new ZeroPaddedInputStream(input);
fakeHeaderBytesIn = new ByteArrayInputStream(fakeHeaderBytes);
sin = new org.icepdf.core.io.SequenceInputStream(fakeHeaderBytesIn, input);
img = deriveBufferedImageFromTIFFBytes(sin, library, lengthOfCompressedData, width, height, compression);
if (img != null) {
break;
}
}
}
} else {
int width = library.getInt(streamDictionary, ImageStream.WIDTH_KEY);
int height = library.getInt(streamDictionary, ImageStream.HEIGHT_KEY);
int approxLen = width * height;
img = deriveBufferedImageFromTIFFBytes(input, library, approxLen, width, height, compression);
}
if (img != null) {
img = applyImageMaskAndDecodeArray(img, imageMask, blackIs1, decodeArray, fill);
}
return img;
}
/**
* Calling code assumes that this method will trap all exceptions,
* so that null shows it didn't work
*
* @param in InputStream to TIFF byte data
* @return RenderedImage if could derive one, else null
*/
private static BufferedImage deriveBufferedImageFromTIFFBytes(
InputStream in, Library library, int compressedBytes, int width, int height, int compression) throws InvocationTargetException, IllegalAccessException {
BufferedImage img = null;
try {
/*
com.sun.media.jai.codec.SeekableStream s = com.sun.media.jai.codec.SeekableStream.wrapInputStream( in, true );
ParameterBlock pb = new ParameterBlock();
pb.add( s );
javax.media.jai.RenderedOp op = javax.media.jai.JAI.create( "tiff", pb );
*/
Object com_sun_media_jai_codec_SeekableStream_s = ssWrapInputStream.invoke(null, in, Boolean.TRUE);
ParameterBlock pb = new ParameterBlock();
pb.add(com_sun_media_jai_codec_SeekableStream_s);
Object javax_media_jai_RenderedOp_op = jaiCreate.invoke(null, "tiff", pb);
/*
* This was another approach:
TIFFDecodeParam tiffDecodeParam = new TIFFDecodeParam();
// tiffDecodeParam.setDecodePaletteAsShorts(true);
ImageDecoder dec = ImageCodec.createImageDecoder("TIFF", s, tiffDecodeParam );
NullOpImage op = new NullOpImage( dec.decodeAsRenderedImage(0), null, null, OpImage.OP_IO_BOUND );
// RenderedImage img = dec.decodeAsRenderedImage();
// RenderedImageAdapter ria = new RenderedImageAdapter(img);
// BufferedImage bi = ria.getAsBufferedImage();
*/
if (javax_media_jai_RenderedOp_op != null) {
if (logger.isLoggable(Level.FINER)) {
logger.fine("Decoding TIFF: " + TIFF_COMPRESSION_NAMES[compression]);
}
// This forces the image to decode, so we can see if that fails,
// and then potentially try a different compression setting
/* op.getTile( 0, 0 ); */
RenderedImage ri = (RenderedImage) javax_media_jai_RenderedOp_op;
Raster r = ri.getTile(0, 0);
// Calling op.getAsBufferedImage() causes a spike in memory usage
// For example, for RenderedOp that's 100KB in size, we spike 18MB,
// with 1MB remaining and 17MB getting gc'ed
// So, we try to build it piecemeal instead
//System.out.println("Memory free: " + Runtime.getRuntime().freeMemory() + ", total:" + Runtime.getRuntime().totalMemory() + ", used: " + (Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory()));
if (r instanceof WritableRaster) {
ColorModel cm = ri.getColorModel();
img = new BufferedImage(cm, (WritableRaster) r, false, null);
} else {
/* img = op.getAsBufferedImage(); */
img = (BufferedImage) roGetAsBufferedImage.invoke(javax_media_jai_RenderedOp_op);
}
}
} catch (Throwable e) {
// catch and return a null image so we can try again using a different compression method.
logger.finer("Decoding TIFF: " + TIFF_COMPRESSION_NAMES[compression] + " failed trying alternative");
} finally {
try {
in.close();
} catch (IOException e) {
// keep quiet
}
}
return img;
}
private static BufferedImage applyImageMaskAndDecodeArray(
BufferedImage img, boolean imageMask, Boolean blackIs1, List decode, Color fill) {
// If the image we actually have is monochrome, and so is useful as an image mask
ColorModel cm = img.getColorModel();
if (cm instanceof IndexColorModel && cm.getPixelSize() == 1) {
// From PDF 1.6 spec, concerning ImageMask and Decode array:
// [0 1] (the default for an image mask), a sample value of 0 marks
// the page with the current color, and a 1 leaves the previous
// contents unchanged.
// [1 0] Is the reverse
// In case alpha transparency doesn't work, it'll paint white opaquely
boolean defaultDecode =
(decode == null) ||
(0.0f == ((Number) decode.get(0)).floatValue());
// From empirically testing 6 of the 9 possible combinations of
// BlackIs1 {true, false, not given} and Decode {[0 1], [1 0], not given}
// this is the rule. Unknown combinations:
// BlackIs1=false, Decode=[0 1]
// BlackIs1=false, Decode=[1 0]
// BlackIs1=true, Decode=[0 1]
boolean flag = ((blackIs1 == null) && (!defaultDecode)) ||
((blackIs1 != null) && blackIs1 && (decode == null));
if (imageMask) {
int a = 0x00FFFFFF; // Clear if alpha supported, else white
int[] cmap = new int[]{
(flag ? fill.getRGB() : a),
(flag ? a : fill.getRGB())
};
int transparentIndex = (flag ? 1 : 0);
IndexColorModel icm = new IndexColorModel(
cm.getPixelSize(), // the number of bits each pixel occupies
cmap.length, // the size of the color component arrays
cmap, // the array of color components
0, // the starting offset of the first color component
true, // indicates whether alpha values are contained in the cmap array
transparentIndex, // the index of the fully transparent pixel
cm.getTransferType()); // the data type of the array used to represent pixel values. The data type must be either DataBuffer.TYPE_BYTE or DataBuffer.TYPE_USHORT
img = new BufferedImage(
icm, img.getRaster(), img.isAlphaPremultiplied(), null);
} else {
int[] cmap = new int[]{
(flag ? 0xFF000000 : 0xFFFFFFFF),
(flag ? 0xFFFFFFFF : 0xFF000000)
};
IndexColorModel icm = new IndexColorModel(
cm.getPixelSize(), // the number of bits each pixel occupies
cmap.length, // the size of the color component arrays
cmap, // the array of color components
0, // the starting offset of the first color component
false, // indicates whether alpha values are contained in the cmap array
-1, // the index of the fully transparent pixel
cm.getTransferType()); // the data type of the array used to represent pixel values. The data type must be either DataBuffer.TYPE_BYTE or DataBuffer.TYPE_USHORT
img = new BufferedImage(
icm, img.getRaster(), img.isAlphaPremultiplied(), null);
}
}
return img;
}
/*
public static void showRenderedImage(java.awt.image.RenderedImage ri, String frameTitle) {
System.out.println("showRenderedImage() \"" + frameTitle + "\"");
// java.awt.Component djai = new com.sun.media.jai.widget.DisplayJAI( ri );
java.awt.Component djai = null;
try {
Class displayClass = Class.forName("com.sun.media.jai.widget.DisplayJAI");
if( displayClass != null ) {
java.lang.reflect.Constructor ctor = displayClass.getConstructor(
new Class[] { java.awt.image.RenderedImage.class } );
djai = (java.awt.Component) ctor.newInstance( new Object[] { ri } );
}
}
catch(Exception e) {
System.out.println("showRenderedImage() problem with JAI: " + e);
return;
}
javax.swing.JFrame testFrame = new javax.swing.JFrame( frameTitle );
testFrame.getContentPane().add( new javax.swing.JScrollPane(djai) );
testFrame.pack();
testFrame.setSize( new java.awt.Dimension(900,800) );
testFrame.setVisible( true );
System.out.println("showRenderedImage() shown");
}
*/
}