blob: b1ab75381470f9cfd44e1a57b4b2544dde09d8e3 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.pdfbox.jbig2.decoder.mmr;
import java.io.EOFException;
import java.io.IOException;
import java.util.Arrays;
import javax.imageio.stream.ImageInputStream;
import org.apache.pdfbox.jbig2.Bitmap;
/**
* A decompressor for MMR compression.
*/
public class MMRDecompressor {
private int width;
private int height;
/**
* A class encapsulating the compressed raw data.
*/
private final class RunData {
private static final int MAX_RUN_DATA_BUFFER = 1024 << 7; // 1024 * 128
private static final int MIN_RUN_DATA_BUFFER = 3; // min. bytes to decompress
private static final int CODE_OFFSET = 24;
/** Compressed data stream. */
ImageInputStream stream;
int offset;
int lastOffset = 0;
int lastCode = 0;
byte buffer[];
int bufferBase;
int bufferTop;
RunData(ImageInputStream stream) {
this.stream = stream;
offset = 0;
lastOffset = 1;
try {
long len = stream.length();
len = Math.min(Math.max(MIN_RUN_DATA_BUFFER, len), MAX_RUN_DATA_BUFFER);
buffer = new byte[(int) len];
fillBuffer(0);
} catch (IOException e) {
buffer = new byte[10];
e.printStackTrace();
}
}
private final Code uncompressGetCode(Code table[]) {
return uncompressGetCodeLittleEndian(table);
}
private final Code uncompressGetCodeLittleEndian(Code table[]) {
final int code = uncompressGetNextCodeLittleEndian() & 0xffffff;
Code result = table[code >> CODE_OFFSET - FIRST_LEVEL_TABLE_SIZE];
// perform second-level lookup
if (null != result && null != result.subTable) {
result = result.subTable[(code >> CODE_OFFSET - FIRST_LEVEL_TABLE_SIZE - SECOND_LEVEL_TABLE_SIZE)
& SECOND_LEVEL_TABLE_MASK];
}
return result;
}
/**
* Fill up the code word in little endian mode. This is a hotspot, therefore the algorithm is
* heavily optimised. For the frequent cases (i.e. short words) we try to get away with as
* little work as possible. <br>
* This method returns code words of 16 bits, which are aligned to the 24th bit. The lowest 8
* bits are used as a "queue" of bits so that an access to the actual data is only needed, when
* this queue becomes empty.
*/
private final int uncompressGetNextCodeLittleEndian() {
try {
// the number of bits to fill (offset difference)
int bitsToFill = offset - lastOffset;
// check whether we can refill, or need to fill in absolute mode
if (bitsToFill < 0 || bitsToFill > 24) {
// refill at absolute offset
int byteOffset = (offset >> 3) - bufferBase; // offset>>3 is equivalent to offset/8
if (byteOffset >= bufferTop) {
byteOffset += bufferBase;
fillBuffer(byteOffset);
byteOffset -= bufferBase;
}
lastCode = (buffer[byteOffset] & 0xff) << 16 | (buffer[byteOffset + 1] & 0xff) << 8
| (buffer[byteOffset + 2] & 0xff);
int bitOffset = offset & 7; // equivalent to offset%8
lastCode <<= bitOffset;
} else {
// the offset to the next byte boundary as seen from the last offset
int bitOffset = lastOffset & 7;
final int avail = 7 - bitOffset;
// check whether there are enough bits in the "queue"
if (bitsToFill <= avail) {
lastCode <<= bitsToFill;
} else {
int byteOffset = (lastOffset >> 3) + 3 - bufferBase;
if (byteOffset >= bufferTop) {
byteOffset += bufferBase;
fillBuffer(byteOffset);
byteOffset -= bufferBase;
}
bitOffset = 8 - bitOffset;
do {
lastCode <<= bitOffset;
lastCode |= buffer[byteOffset] & 0xff;
bitsToFill -= bitOffset;
byteOffset++;
bitOffset = 8;
} while (bitsToFill >= 8);
lastCode <<= bitsToFill; // shift the rest
}
}
lastOffset = offset;
return lastCode;
} catch (IOException e) {
// will this actually happen? only with broken data, I'd say.
throw new ArrayIndexOutOfBoundsException("Corrupted RLE data caused by an IOException while reading raw data: "
+ e.toString());
}
}
private void fillBuffer(int byteOffset) throws IOException {
bufferBase = byteOffset;
synchronized (stream) {
try {
stream.seek(byteOffset);
bufferTop = stream.read(buffer);
} catch (EOFException e) {
// you never know which kind of EOF will kick in
bufferTop = -1;
}
// check filling degree
if (bufferTop > -1 && bufferTop < 3) {
// CK: if filling degree is too small,
// smoothly fill up to the next three bytes or substitute with with
// empty bytes
int read = 0;
while (bufferTop < 3) {
try {
read = stream.read();
} catch (EOFException e) {
read = -1;
}
buffer[bufferTop++] = read == -1 ? 0 : (byte) (read & 0xff);
}
}
}
// leave some room, in order to save a few tests in the calling code
bufferTop -= 3;
if (bufferTop < 0) {
// if we're at EOF, just supply zero-bytes
Arrays.fill(buffer, (byte) 0);
bufferTop = buffer.length - 3;
}
}
/**
* Skip to next byte
*/
private void align() {
offset = ((offset + 7) >> 3) << 3;
}
}
private static final class Code {
Code subTable[] = null;
final int bitLength, codeWord, runLength;
Code(int codeData[]) {
bitLength = codeData[0];
codeWord = codeData[1];
runLength = codeData[2];
}
public String toString() {
return bitLength + "/" + codeWord + "/" + runLength;
}
/**
* @see java.lang.Object#equals(Object)
*/
public boolean equals(Object obj) {
return (obj instanceof Code) && //
((Code) obj).bitLength == bitLength && //
((Code) obj).codeWord == codeWord && //
((Code) obj).runLength == runLength;
}
}
private static final int FIRST_LEVEL_TABLE_SIZE = 8;
private static final int FIRST_LEVEL_TABLE_MASK = (1 << FIRST_LEVEL_TABLE_SIZE) - 1;
private static final int SECOND_LEVEL_TABLE_SIZE = 5;
private static final int SECOND_LEVEL_TABLE_MASK = (1 << SECOND_LEVEL_TABLE_SIZE) - 1;
private static Code whiteTable[] = null;
private static Code blackTable[] = null;
private static Code modeTable[] = null;
private RunData data;
private synchronized final static void initTables() {
if (null == whiteTable) {
whiteTable = createLittleEndianTable(MMRConstants.WhiteCodes);
blackTable = createLittleEndianTable(MMRConstants.BlackCodes);
modeTable = createLittleEndianTable(MMRConstants.ModeCodes);
}
}
private final int uncompress2D(RunData runData, int[] referenceOffsets, int refRunLength, int[] runOffsets, int width) {
int referenceBufferOffset = 0;
int currentBufferOffset = 0;
int currentLineBitPosition = 0;
boolean whiteRun = true; // Always start with a white run
Code code = null; // Storage var for current code being processed
referenceOffsets[refRunLength] = referenceOffsets[refRunLength + 1] = width;
referenceOffsets[refRunLength + 2] = referenceOffsets[refRunLength + 3] = width + 1;
try {
decodeLoop : while (currentLineBitPosition < width) {
// Get the mode code
code = runData.uncompressGetCode(modeTable);
if (code == null) {
runData.offset++;
break decodeLoop;
}
// Add the code length to the bit offset
runData.offset += code.bitLength;
switch (code.runLength){
case MMRConstants.CODE_V0 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset];
break;
case MMRConstants.CODE_VR1 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset] + 1;
break;
case MMRConstants.CODE_VL1 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset] - 1;
break;
case MMRConstants.CODE_H :
for (int ever = 1; ever > 0;) {
code = runData.uncompressGetCode(whiteRun == true ? whiteTable : blackTable);
if (code == null)
break decodeLoop;
runData.offset += code.bitLength;
if (code.runLength < 64) {
if (code.runLength < 0) {
runOffsets[currentBufferOffset++] = currentLineBitPosition;
code = null;
break decodeLoop;
}
currentLineBitPosition += code.runLength;
runOffsets[currentBufferOffset++] = currentLineBitPosition;
break;
}
currentLineBitPosition += code.runLength;
}
final int firstHalfBitPos = currentLineBitPosition;
for (int ever1 = 1; ever1 > 0;) {
code = runData.uncompressGetCode(whiteRun != true ? whiteTable : blackTable);
if (code == null)
break decodeLoop;
runData.offset += code.bitLength;
if (code.runLength < 64) {
if (code.runLength < 0) {
runOffsets[currentBufferOffset++] = currentLineBitPosition;
break decodeLoop;
}
currentLineBitPosition += code.runLength;
// don't generate 0-length run at EOL for cases where the line ends in an H-run.
if (currentLineBitPosition < width || currentLineBitPosition != firstHalfBitPos)
runOffsets[currentBufferOffset++] = currentLineBitPosition;
break;
}
currentLineBitPosition += code.runLength;
}
while (currentLineBitPosition < width && referenceOffsets[referenceBufferOffset] <= currentLineBitPosition) {
referenceBufferOffset += 2;
}
continue decodeLoop;
case MMRConstants.CODE_P :
referenceBufferOffset++;
currentLineBitPosition = referenceOffsets[referenceBufferOffset++];
continue decodeLoop;
case MMRConstants.CODE_VR2 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset] + 2;
break;
case MMRConstants.CODE_VL2 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset] - 2;
break;
case MMRConstants.CODE_VR3 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset] + 3;
break;
case MMRConstants.CODE_VL3 :
currentLineBitPosition = referenceOffsets[referenceBufferOffset] - 3;
break;
case MMRConstants.EOL :
default :
System.err.println("Should not happen!");
// Possibly MMR Decoded
if (runData.offset == 12 && code.runLength == MMRConstants.EOL) {
runData.offset = 0;
uncompress1D(runData, referenceOffsets, width);
runData.offset++;
uncompress1D(runData, runOffsets, width);
int retCode = uncompress1D(runData, referenceOffsets, width);
runData.offset++;
return retCode;
}
currentLineBitPosition = width;
continue decodeLoop;
}
// Only vertical modes get this far
if (currentLineBitPosition <= width) {
whiteRun = !whiteRun;
runOffsets[currentBufferOffset++] = currentLineBitPosition;
if (referenceBufferOffset > 0) {
referenceBufferOffset--;
} else {
referenceBufferOffset++;
}
while (currentLineBitPosition < width && referenceOffsets[referenceBufferOffset] <= currentLineBitPosition) {
referenceBufferOffset += 2;
}
}
}
} catch (Throwable t) {
StringBuffer strBuf = new StringBuffer();
strBuf.append("whiteRun = ");
strBuf.append(whiteRun);
strBuf.append("\n");
strBuf.append("code = ");
strBuf.append(code);
strBuf.append("\n");
strBuf.append("refOffset = ");
strBuf.append(referenceBufferOffset);
strBuf.append("\n");
strBuf.append("curOffset = ");
strBuf.append(currentBufferOffset);
strBuf.append("\n");
strBuf.append("bitPos = ");
strBuf.append(currentLineBitPosition);
strBuf.append("\n");
strBuf.append("runData.offset = ");
strBuf.append(runData.offset);
strBuf.append(" ( byte:");
strBuf.append(runData.offset / 8);
strBuf.append(", bit:");
strBuf.append(runData.offset & 0x07);
strBuf.append(" )");
System.out.println(strBuf.toString());
return MMRConstants.EOF;
}
if (runOffsets[currentBufferOffset] != width) {
runOffsets[currentBufferOffset] = width;
}
if (code == null) {
return MMRConstants.EOL;
}
return currentBufferOffset;
}
public MMRDecompressor(int width, int height, ImageInputStream stream) {
this.width = width;
this.height = height;
data = new RunData(stream);
initTables();
}
public Bitmap uncompress() {
final Bitmap result = new Bitmap(width, height);
int[] currentOffsets = new int[width + 5];
int[] referenceOffsets = new int[width + 5];
referenceOffsets[0] = width;
int refRunLength = 1;
int count = 0;
for (int line = 0; line < height; line++) {
count = uncompress2D(data, referenceOffsets, refRunLength, currentOffsets, width);
if (count == MMRConstants.EOF) {
break;
}
if (count > 0) {
fillBitmap(result, line, currentOffsets, count);
}
// Swap lines
int tempOffsets[] = referenceOffsets;
referenceOffsets = currentOffsets;
currentOffsets = tempOffsets;
refRunLength = count;
}
detectAndSkipEOL();
data.align();
return result;
}
private void detectAndSkipEOL() {
while (true) {
Code code = data.uncompressGetCode(modeTable);
if (null != code && code.runLength == MMRConstants.EOL) {
data.offset += code.bitLength;
} else
break;
}
}
private void fillBitmap(Bitmap result, int line, int[] currentOffsets, int count) {
int x = 0;
int targetByte = result.getByteIndex(0, line);
byte targetByteValue = 0;
for (int index = 0; index < count; index++) {
final int offset = currentOffsets[index];
byte value;
if ((index & 1) == 0) {
value = 0;
} else {
value = 1;
}
while (x < offset) {
targetByteValue = (byte) ((targetByteValue << 1) | value);
x++;
if ((x & 7) == 0) {
result.setByte(targetByte++, targetByteValue);
targetByteValue = 0;
}
}
}
if ((x & 7) != 0) {
targetByteValue <<= 8 - (x & 7);
result.setByte(targetByte, targetByteValue);
}
}
private final int uncompress1D(RunData runData, int[] runOffsets, int width) {
boolean whiteRun = true;
int iBitPos = 0;
Code code = null;
int refOffset = 0;
loop : while (iBitPos < width) {
while (true) {
if (whiteRun) {
code = runData.uncompressGetCode(whiteTable);
} else {
code = runData.uncompressGetCode(blackTable);
}
runData.offset += code.bitLength;
if (code.runLength < 0) {
break loop;
}
iBitPos += code.runLength;
if (code.runLength < 64) {
whiteRun = !whiteRun;
runOffsets[refOffset++] = iBitPos;
break;
}
}
}
if (runOffsets[refOffset] != width) {
runOffsets[refOffset] = width;
}
return code != null && code.runLength != MMRConstants.EOL ? refOffset : MMRConstants.EOL;
}
/**
* For little endian, the tables are structured like this:
*
* <pre>
* v--------v length = FIRST_LEVEL_TABLE_LENGTH
* v-----v length = SECOND_LEVEL_TABLE_LENGTH
*
* A code word which fits into the first level table (length=3)
* [Cccvvvvv]
*
* A code word which needs the second level table also (length=10)
* [Cccccccc] -&gt; [ccvvv]
*
* &quot;C&quot; denotes the first code word bit
* &quot;c&quot; denotes a code word bit
* &quot;v&quot; denotes a variant bit
* </pre>
*
*/
private static Code[] createLittleEndianTable(int codes[][]) {
final Code firstLevelTable[] = new Code[FIRST_LEVEL_TABLE_MASK + 1];
for (int i = 0; i < codes.length; i++) {
final Code code = new Code(codes[i]);
if (code.bitLength <= FIRST_LEVEL_TABLE_SIZE) {
final int variantLength = FIRST_LEVEL_TABLE_SIZE - code.bitLength;
final int baseWord = code.codeWord << variantLength;
for (int variant = (1 << variantLength) - 1; variant >= 0; variant--) {
final int index = baseWord | variant;
firstLevelTable[index] = code;
}
} else {
// init second level table
final int firstLevelIndex = code.codeWord >>> code.bitLength - FIRST_LEVEL_TABLE_SIZE;
if (firstLevelTable[firstLevelIndex] == null) {
final Code firstLevelCode = new Code(new int[3]);
firstLevelCode.subTable = new Code[SECOND_LEVEL_TABLE_MASK + 1];
firstLevelTable[firstLevelIndex] = firstLevelCode;
}
// fill second level table
if (code.bitLength <= FIRST_LEVEL_TABLE_SIZE + SECOND_LEVEL_TABLE_SIZE) {
final Code secondLevelTable[] = firstLevelTable[firstLevelIndex].subTable;
final int variantLength = FIRST_LEVEL_TABLE_SIZE + SECOND_LEVEL_TABLE_SIZE - code.bitLength;
final int baseWord = (code.codeWord << variantLength) & SECOND_LEVEL_TABLE_MASK;
for (int variant = (1 << variantLength) - 1; variant >= 0; variant--) {
secondLevelTable[baseWord | variant] = code;
}
} else
throw new IllegalArgumentException("Code table overflow in MMRDecompressor");
}
}
return firstLevelTable;
}
}