275 lines
7.1 KiB
Java
275 lines
7.1 KiB
Java
package ar.com.hjg.pngj.pixels;
|
|
|
|
import java.nio.ByteOrder;
|
|
|
|
/**
|
|
* This estimator actually uses the LZ4 compression algorithm, and hopes that
|
|
* it's well correlated with Deflater. It's
|
|
* about 3 to 4 times faster than Deflater.
|
|
*
|
|
* This is a modified heavily trimmed version of the
|
|
* net.jpountz.lz4.LZ4JavaSafeCompressor class plus some methods from
|
|
* other classes from LZ4 Java library: https://github.com/jpountz/lz4-java ,
|
|
* originally licensed under the Apache
|
|
* License 2.0
|
|
*/
|
|
final public class DeflaterEstimatorLz4 {
|
|
|
|
/**
|
|
* This object is stateless, it's thread safe and can be reused
|
|
*/
|
|
public DeflaterEstimatorLz4() {}
|
|
|
|
/**
|
|
* Estimates the length of the compressed bytes, as compressed by Lz4
|
|
* WARNING: if larger than LZ4_64K_LIMIT it cuts it
|
|
* in fragments
|
|
*
|
|
* WARNING: if some part of the input is discarded, this should return the
|
|
* proportional (so that
|
|
* returnValue/srcLen=compressionRatio)
|
|
*
|
|
* @param src
|
|
* @param srcOff
|
|
* @param srcLen
|
|
* @return length of the compressed bytes
|
|
*/
|
|
public int compressEstim(byte[] src, int srcOff, final int srcLen) {
|
|
if (srcLen < 10)
|
|
return srcLen; // too small
|
|
int stride = LZ4_64K_LIMIT - 1;
|
|
int segments = (srcLen + stride - 1) / stride;
|
|
stride = srcLen / segments;
|
|
if (stride >= LZ4_64K_LIMIT - 1 || stride * segments > srcLen || segments < 1 || stride < 1)
|
|
throw new RuntimeException("?? " + srcLen);
|
|
int bytesIn = 0;
|
|
int bytesOut = 0;
|
|
int len = srcLen;
|
|
while (len > 0) {
|
|
if (len > stride)
|
|
len = stride;
|
|
bytesOut += compress64k(src, srcOff, len);
|
|
srcOff += len;
|
|
bytesIn += len;
|
|
len = srcLen - bytesIn;
|
|
}
|
|
double ratio = bytesOut / (double) bytesIn;
|
|
return bytesIn == srcLen ? bytesOut : (int) (ratio * srcLen + 0.5);
|
|
}
|
|
|
|
public int compressEstim(byte[] src) {
|
|
return compressEstim(src, 0, src.length);
|
|
}
|
|
|
|
static final ByteOrder NATIVE_BYTE_ORDER = ByteOrder.nativeOrder();
|
|
|
|
static final int MEMORY_USAGE = 14;
|
|
static final int NOT_COMPRESSIBLE_DETECTION_LEVEL = 6;
|
|
|
|
static final int MIN_MATCH = 4;
|
|
|
|
static final int HASH_LOG = MEMORY_USAGE - 2;
|
|
static final int HASH_TABLE_SIZE = 1 << HASH_LOG;
|
|
|
|
static final int SKIP_STRENGTH = Math.max(NOT_COMPRESSIBLE_DETECTION_LEVEL, 2);
|
|
static final int COPY_LENGTH = 8;
|
|
static final int LAST_LITERALS = 5;
|
|
static final int MF_LIMIT = COPY_LENGTH + MIN_MATCH;
|
|
static final int MIN_LENGTH = MF_LIMIT + 1;
|
|
|
|
static final int MAX_DISTANCE = 1 << 16;
|
|
|
|
static final int ML_BITS = 4;
|
|
static final int ML_MASK = (1 << ML_BITS) - 1;
|
|
static final int RUN_BITS = 8 - ML_BITS;
|
|
static final int RUN_MASK = (1 << RUN_BITS) - 1;
|
|
|
|
static final int LZ4_64K_LIMIT = (1 << 16) + (MF_LIMIT - 1);
|
|
static final int HASH_LOG_64K = HASH_LOG + 1;
|
|
static final int HASH_TABLE_SIZE_64K = 1 << HASH_LOG_64K;
|
|
|
|
static final int HASH_LOG_HC = 15;
|
|
static final int HASH_TABLE_SIZE_HC = 1 << HASH_LOG_HC;
|
|
static final int OPTIMAL_ML = ML_MASK - 1 + MIN_MATCH;
|
|
|
|
static int compress64k(byte[] src, int srcOff, int srcLen) {
|
|
final int srcEnd = srcOff + srcLen;
|
|
final int srcLimit = srcEnd - LAST_LITERALS;
|
|
final int mflimit = srcEnd - MF_LIMIT;
|
|
|
|
int sOff = srcOff, dOff = 0;
|
|
|
|
int anchor = sOff;
|
|
|
|
if (srcLen >= MIN_LENGTH) {
|
|
|
|
final short[] hashTable = new short[HASH_TABLE_SIZE_64K];
|
|
|
|
++sOff;
|
|
|
|
main: while (true) {
|
|
|
|
// find a match
|
|
int forwardOff = sOff;
|
|
|
|
int ref;
|
|
int findMatchAttempts = (1 << SKIP_STRENGTH) + 3;
|
|
do {
|
|
sOff = forwardOff;
|
|
forwardOff += findMatchAttempts++ >>> SKIP_STRENGTH;
|
|
|
|
if (forwardOff > mflimit) {
|
|
break main;
|
|
}
|
|
|
|
final int h = hash64k(readInt(src, sOff));
|
|
ref = srcOff + readShort(hashTable, h);
|
|
writeShort(hashTable, h, sOff - srcOff);
|
|
} while (!readIntEquals(src, ref, sOff));
|
|
|
|
// catch up
|
|
final int excess = commonBytesBackward(src, ref, sOff, srcOff, anchor);
|
|
sOff -= excess;
|
|
ref -= excess;
|
|
// sequence == refsequence
|
|
final int runLen = sOff - anchor;
|
|
dOff++;
|
|
|
|
if (runLen >= RUN_MASK) {
|
|
if (runLen > RUN_MASK)
|
|
dOff += (runLen - RUN_MASK) / 0xFF;
|
|
dOff++;
|
|
}
|
|
dOff += runLen;
|
|
while (true) {
|
|
// encode offset
|
|
dOff += 2;
|
|
// count nb matches
|
|
sOff += MIN_MATCH;
|
|
ref += MIN_MATCH;
|
|
final int matchLen = commonBytes(src, ref, sOff, srcLimit);
|
|
sOff += matchLen;
|
|
// encode match len
|
|
if (matchLen >= ML_MASK) {
|
|
if (matchLen >= ML_MASK + 0xFF)
|
|
dOff += (matchLen - ML_MASK) / 0xFF;
|
|
dOff++;
|
|
}
|
|
// test end of chunk
|
|
if (sOff > mflimit) {
|
|
anchor = sOff;
|
|
break main;
|
|
}
|
|
// fill table
|
|
writeShort(hashTable, hash64k(readInt(src, sOff - 2)), sOff - 2 - srcOff);
|
|
// test next position
|
|
final int h = hash64k(readInt(src, sOff));
|
|
ref = srcOff + readShort(hashTable, h);
|
|
writeShort(hashTable, h, sOff - srcOff);
|
|
if (!readIntEquals(src, sOff, ref)) {
|
|
break;
|
|
}
|
|
dOff++;
|
|
}
|
|
// prepare next loop
|
|
anchor = sOff++;
|
|
}
|
|
}
|
|
int runLen = srcEnd - anchor;
|
|
if (runLen >= RUN_MASK + 0xFF) {
|
|
dOff += (runLen - RUN_MASK) / 0xFF;
|
|
}
|
|
dOff++;
|
|
dOff += runLen;
|
|
return dOff;
|
|
}
|
|
|
|
static final int maxCompressedLength(int length) {
|
|
if (length < 0) {
|
|
throw new IllegalArgumentException("length must be >= 0, got " + length);
|
|
}
|
|
return length + length / 255 + 16;
|
|
}
|
|
|
|
static int hash(int i) {
|
|
return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG);
|
|
}
|
|
|
|
static int hash64k(int i) {
|
|
return (i * -1640531535) >>> ((MIN_MATCH * 8) - HASH_LOG_64K);
|
|
}
|
|
|
|
static int readShortLittleEndian(byte[] buf, int i) {
|
|
return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8);
|
|
}
|
|
|
|
static boolean readIntEquals(byte[] buf, int i, int j) {
|
|
return buf[i] == buf[j] && buf[i + 1] == buf[j + 1] && buf[i + 2] == buf[j + 2] && buf[i + 3] == buf[j + 3];
|
|
}
|
|
|
|
static int commonBytes(byte[] b, int o1, int o2, int limit) {
|
|
int count = 0;
|
|
while (o2 < limit && b[o1++] == b[o2++]) {
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static int commonBytesBackward(byte[] b, int o1, int o2, int l1, int l2) {
|
|
int count = 0;
|
|
while (o1 > l1 && o2 > l2 && b[--o1] == b[--o2]) {
|
|
++count;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
static int readShort(short[] buf, int off) {
|
|
return buf[off] & 0xFFFF;
|
|
}
|
|
|
|
static byte readByte(byte[] buf, int i) {
|
|
return buf[i];
|
|
}
|
|
|
|
static void checkRange(byte[] buf, int off) {
|
|
if (off < 0 || off >= buf.length) {
|
|
throw new ArrayIndexOutOfBoundsException(off);
|
|
}
|
|
}
|
|
|
|
static void checkRange(byte[] buf, int off, int len) {
|
|
checkLength(len);
|
|
if (len > 0) {
|
|
checkRange(buf, off);
|
|
checkRange(buf, off + len - 1);
|
|
}
|
|
}
|
|
|
|
static void checkLength(int len) {
|
|
if (len < 0) {
|
|
throw new IllegalArgumentException("lengths must be >= 0");
|
|
}
|
|
}
|
|
|
|
static int readIntBE(byte[] buf, int i) {
|
|
return ((buf[i] & 0xFF) << 24) | ((buf[i + 1] & 0xFF) << 16) | ((buf[i + 2] & 0xFF) << 8) | (buf[i + 3] & 0xFF);
|
|
}
|
|
|
|
static int readIntLE(byte[] buf, int i) {
|
|
return (buf[i] & 0xFF) | ((buf[i + 1] & 0xFF) << 8) | ((buf[i + 2] & 0xFF) << 16) | ((buf[i + 3] & 0xFF) << 24);
|
|
}
|
|
|
|
static int readInt(byte[] buf, int i) {
|
|
if (NATIVE_BYTE_ORDER == ByteOrder.BIG_ENDIAN) {
|
|
return readIntBE(buf, i);
|
|
} else {
|
|
return readIntLE(buf, i);
|
|
}
|
|
}
|
|
|
|
static void writeShort(short[] buf, int off, int v) {
|
|
buf[off] = (short) v;
|
|
}
|
|
|
|
}
|