diff --git a/ij/io/FileInfo.java b/ij/io/FileInfo.java index ad54a3dd..e1a562e3 100644 --- a/ij/io/FileInfo.java +++ b/ij/io/FileInfo.java @@ -94,7 +94,9 @@ public class FileInfo implements Cloneable { public static final int PACK_BITS = 5; public static final int ZIP = 6; public static final int ZIP_WITH_DIFFERENCING = 7; - + public static final int ZSTD = 8; + public static final int ZSTD_WITH_DIFFERENCING = 9; + /* File format (TIFF, GIF_OR_JPG, BMP, etc.). Used by the File/Revert command */ public int fileFormat; diff --git a/ij/io/ImageReader.java b/ij/io/ImageReader.java index 609f0bb1..de51600b 100644 --- a/ij/io/ImageReader.java +++ b/ij/io/ImageReader.java @@ -84,7 +84,7 @@ else if (i > 0) { byteArray = uncompress(byteArray); int length = byteArray.length; length = length - (length%fi.width); - if (fi.compression==FileInfo.LZW_WITH_DIFFERENCING||fi.compression==FileInfo.ZIP_WITH_DIFFERENCING) { + if (fi.compression==FileInfo.LZW_WITH_DIFFERENCING||fi.compression==FileInfo.ZIP_WITH_DIFFERENCING||fi.compression==FileInfo.ZSTD_WITH_DIFFERENCING) { for (int b=0; b 0) { for (int i=base,j=0; i0 && declared<=Integer.MAX_VALUE; + int capacity = exact?(int)declared:expectedSize; + if (capacity<=0) + capacity = Math.max(1<<16, input.length*4); + for (int attempt=0; ; attempt++) { + try { + byte[] output = new byte[capacity]; + int n = backend.decompress(input, output); + if (n<0 || n>capacity) + throw new IllegalStateException("bad decompressed length "+n); + return n==capacity?output:Arrays.copyOf(output, n); + } catch (OutOfMemoryError e) { + throw e; + } catch (Throwable e) { + // Only an estimated capacity is worth retrying; a declared size is + // authoritative, so a failure there is a genuinely corrupt frame. + if (exact || attempt>=6 || capacity>(1<<28)) { + IJ.log("ZSTD decompression failed: "+e); + return null; + } + capacity *= 4; + } + } + } + + private static void init() { + if (initialized) + return; + initialized = true; + try { + backend = new ZstdJni(); + return; + } catch (Throwable e) {} + try { + backend = new AirCompressor(); + } catch (Throwable e) {} + } + + private interface Backend { + String name(); + /** Decompressed size declared in the frame header, or 0 if not declared. */ + long declaredSize(byte[] input); + /** Decompresses into output; returns the number of bytes written. */ + int decompress(byte[] input, byte[] output) throws Exception; + } + + /** zstd-jni: static methods on com.github.luben.zstd.Zstd. */ + private static class ZstdJni implements Backend { + private final Method decompressByteArray, decompressedSize; + + ZstdJni() throws Exception { + Class c = Class.forName("com.github.luben.zstd.Zstd"); + decompressByteArray = c.getMethod("decompressByteArray", + byte[].class, int.class, int.class, byte[].class, int.class, int.class); + decompressedSize = c.getMethod("decompressedSize", byte[].class); + // Call a native method now, so that a jar without a binary for this + // platform makes us fall through to the pure-Java backend here rather + // than failing later, in the middle of reading an image. + c.getMethod("maxCompressionLevel").invoke(null); + } + + public String name() {return "zstd-jni";} + + public long declaredSize(byte[] input) { + try { + Object r = decompressedSize.invoke(null, (Object)input); + return ((Number)r).longValue(); + } catch (Throwable e) { + return 0L; + } + } + + public int decompress(byte[] input, byte[] output) throws Exception { + Object r = decompressByteArray.invoke(null, + output, Integer.valueOf(0), Integer.valueOf(output.length), + input, Integer.valueOf(0), Integer.valueOf(input.length)); + long n = ((Number)r).longValue(); + // zstd-jni signals errors by returning a large negative-coded value. + if (n<0 || n>output.length) + throw new java.io.IOException("zstd-jni error code "+n); + return (int)n; + } + } + + /** aircompressor: pure-Java io.airlift.compress.zstd.ZstdDecompressor. */ + private static class AirCompressor implements Backend { + private final Object instance; + private final Method decompress, getDecompressedSize; + + AirCompressor() throws Exception { + Class c = Class.forName("io.airlift.compress.zstd.ZstdDecompressor"); + instance = c.getConstructor().newInstance(); + decompress = c.getMethod("decompress", + byte[].class, int.class, int.class, byte[].class, int.class, int.class); + getDecompressedSize = c.getMethod("getDecompressedSize", + byte[].class, int.class, int.class); + } + + public String name() {return "aircompressor";} + + public long declaredSize(byte[] input) { + try { + Object r = getDecompressedSize.invoke(null, + input, Integer.valueOf(0), Integer.valueOf(input.length)); + return ((Number)r).longValue(); + } catch (Throwable e) { + return 0L; + } + } + + public int decompress(byte[] input, byte[] output) throws Exception { + Object r = decompress.invoke(instance, + input, Integer.valueOf(0), Integer.valueOf(input.length), + output, Integer.valueOf(0), Integer.valueOf(output.length)); + return ((Number)r).intValue(); + } + } + +}