From 19597b639c36a2dee6dd0840680f8d0ed5cf97d4 Mon Sep 17 00:00:00 2001 From: Melissa Linkert Date: Tue, 7 Dec 2021 15:05:55 -0600 Subject: [PATCH] Initial progress on JPEG2000 compression --- pom.xml | 9 +- src/main/java/com/bc/zarr/Compressor.java | 37 ++++++ .../java/com/bc/zarr/CompressorFactory.java | 119 ++++++++++++++++++ src/test/java/com/bc/zarr/CompressorTest.java | 46 +++++++ 4 files changed, 210 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbfaf54..310045c 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,13 @@ jblosc 1.0.1 + + + org.openmicroscopy + ome-codecs + 0.3.1 + + @@ -211,4 +218,4 @@ - \ No newline at end of file + diff --git a/src/main/java/com/bc/zarr/Compressor.java b/src/main/java/com/bc/zarr/Compressor.java index 71fdcd2..1e88fd8 100644 --- a/src/main/java/com/bc/zarr/Compressor.java +++ b/src/main/java/com/bc/zarr/Compressor.java @@ -50,4 +50,41 @@ void passThrough(InputStream is, OutputStream os) throws IOException { read = is.read(bytes); } } + + public boolean booleanValue(Object v, boolean defaultValue) { + if (v == null) { + return defaultValue; + } + if (v instanceof Boolean) { + return (Boolean) v; + } + return Boolean.parseBoolean(v.toString()); + } + + public int intValue(Object v, int defaultValue) { + int value = defaultValue; + if (v != null) { + if (v instanceof String) { + value = Integer.parseInt((String) v); + } + else if (v instanceof Number) { + value = ((Number) v).intValue(); + } + } + return value; + } + + public double doubleValue(Object v, double defaultValue) { + double value = defaultValue; + if (v != null) { + if (v instanceof String) { + value = Double.parseDouble((String) v); + } + else if (v instanceof Number) { + value = ((Number) v).doubleValue(); + } + } + return value; + } + } diff --git a/src/main/java/com/bc/zarr/CompressorFactory.java b/src/main/java/com/bc/zarr/CompressorFactory.java index e9c3c76..1e99a8d 100644 --- a/src/main/java/com/bc/zarr/CompressorFactory.java +++ b/src/main/java/com/bc/zarr/CompressorFactory.java @@ -31,6 +31,7 @@ import org.blosc.IBloscDll; import org.blosc.JBlosc; +import java.awt.image.WritableRaster; import java.io.ByteArrayOutputStream; import java.io.DataInput; import java.io.DataInputStream; @@ -47,6 +48,16 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import loci.common.services.DependencyException; +import loci.common.services.ServiceException; +import loci.common.services.ServiceFactory; +import ome.codecs.CodecException; +import ome.codecs.JPEG2000Codec; +import ome.codecs.JPEG2000CodecOptions; +import ome.codecs.gui.AWTImageTools; +import ome.codecs.services.JAIIIOService; +import ome.codecs.services.JAIIIOServiceImpl; + public class CompressorFactory { public final static Compressor nullCompressor = new NullCompressor(); @@ -120,6 +131,9 @@ public static Compressor create(String id, Map properties) { if ("blosc".equals(id)) { return new BloscCompressor(properties); } + if ("j2k".equals(id)) { + return new J2KCompressor(properties); + } throw new IllegalArgumentException("Compressor id:'" + id + "' not supported."); } @@ -356,5 +370,110 @@ private BufferSizes cbufferSizes(ByteBuffer cbuffer) { return bs; } } + + static class J2KCompressor extends Compressor { + public static final String littleEndianKey = "littleEndian"; + public static final String interleavedKey = "interleaved"; + public static final String losslessKey = "lossless"; + public static final String widthKey = "imageWidth"; + public static final String heightKey = "imageHeight"; + public static final String bitsPerSampleKey = "bitsPerSample"; + public static final String channelsKey = "channels"; + public static final String qualityKey = "quality"; + + // TODO: any way to copy these options from array metadata? + private boolean littleEndian; + private boolean interleaved; + private int width; + private int height; + private int bitsPerSample; + private int channels; + // specify lossless or quality, not both + private boolean lossless; + private double quality; + + private J2KCompressor(Map map) { + littleEndian = booleanValue(map.get(littleEndianKey), false); + interleaved = booleanValue(map.get(interleavedKey), false); + width = intValue(map.get(widthKey), -1); + height = intValue(map.get(heightKey), -1); + bitsPerSample = intValue(map.get(bitsPerSampleKey), 8); + channels = intValue(map.get(channelsKey), 1); + + // if neither quality nor lossless is defined, do lossless compression + quality = doubleValue(map.get(qualityKey), -1); + lossless = booleanValue(map.get(losslessKey), quality < 0); + } + + @Override + public String toString() { + return "compressor=" + getId(); + } + + @Override + public String getId() { + return "j2k"; + } + + @Override + public void compress(InputStream is, OutputStream os) throws IOException { + try (ByteArrayOutputStream tmpOut = new ByteArrayOutputStream()) { + passThrough(is, tmpOut); + byte[] buffer = tmpOut.toByteArray(); + JPEG2000Codec codec = new JPEG2000Codec(); + JPEG2000CodecOptions options = getCodecOptions(); + byte[] compressed = codec.compress(buffer, options); + os.write(compressed); + } + catch (CodecException e) { + throw new IOException(e); + } + } + + @Override + public void uncompress(InputStream is, OutputStream os) throws IOException { + try { + JAIIIOService service = getService(); + WritableRaster raster = (WritableRaster) service.readRaster(is, getCodecOptions()); + byte[][] raw = AWTImageTools.getPixelBytes(raster, littleEndian); + for (byte[] channel : raw) { + os.write(channel); + } + } + catch (ServiceException e) { + throw new IOException(e); + } + } + + private JPEG2000CodecOptions getCodecOptions() { + JPEG2000CodecOptions options = new JPEG2000CodecOptions(); + options.interleaved = interleaved; + options.littleEndian = littleEndian; + options.width = width; + options.height = height; + options.channels = channels; + options.bitsPerSample = bitsPerSample; + if (quality >= 0) { + options.quality = quality; + } + options.lossless = lossless; + options.numDecompositionLevels = 1; + + return JPEG2000CodecOptions.getDefaultOptions(options); + } + + private JAIIIOService getService() throws IOException { + try { + ServiceFactory factory = new ServiceFactory(); + return factory.getInstance(JAIIIOService.class); + } + catch (DependencyException de) { + throw new IOException(JAIIIOServiceImpl.NO_J2K_MSG, de); + } + } + + } + + } diff --git a/src/test/java/com/bc/zarr/CompressorTest.java b/src/test/java/com/bc/zarr/CompressorTest.java index 4324664..e52affb 100644 --- a/src/test/java/com/bc/zarr/CompressorTest.java +++ b/src/test/java/com/bc/zarr/CompressorTest.java @@ -28,6 +28,7 @@ import static org.hamcrest.Matchers.*; import static org.hamcrest.MatcherAssert.*; +import static org.junit.Assert.assertNotNull; import com.bc.zarr.chunk.ZarrInputStreamAdapter; @@ -155,6 +156,51 @@ public void writeRead_BloscCompressor() throws IOException { assertThat(input, is(equalTo(uncompressed))); } + @Test + public void writeRead_J2KCompressor() throws IOException { + final Map j2kProperties = new LinkedHashMap<>(); + j2kProperties.put(CompressorFactory.J2KCompressor.widthKey, 11); + j2kProperties.put(CompressorFactory.J2KCompressor.heightKey, 5); + j2kProperties.put(CompressorFactory.J2KCompressor.bitsPerSampleKey, 16); + j2kProperties.put(CompressorFactory.J2KCompressor.channelsKey, 1); + j2kProperties.put(CompressorFactory.J2KCompressor.interleavedKey, false); + j2kProperties.put(CompressorFactory.J2KCompressor.littleEndianKey, false); + j2kProperties.put(CompressorFactory.J2KCompressor.losslessKey, true); + + final Compressor compressor = CompressorFactory.create("j2k", j2kProperties); + final ByteOrder byteOrder = ByteOrder.BIG_ENDIAN; + final short[] input = { + 100, 22, 100, 22, 22, 22, 100, 100, 100, 22, 100, + 100, 22, 100, 22, 22, 22, 100, 100, 100, 22, 100, + 100, 22, 100, 22, 22, 22, 100, 100, 100, 22, 100, + 100, 22, 100, 22, 22, 22, 100, 100, 100, 22, 100, + 100, 22, 100, 22, 22, 22, 100, 100, 100, 22, 100 + }; + final MemoryCacheImageOutputStream iis = new MemoryCacheImageOutputStream(new ByteArrayOutputStream()); + iis.setByteOrder(byteOrder); + iis.writeShorts(input, 0, input.length); + iis.seek(0); + + ByteArrayOutputStream os; + ByteArrayInputStream is; + + //write + os = new ByteArrayOutputStream(); + compressor.compress(new ZarrInputStreamAdapter(iis), os); + final byte[] compressed = os.toByteArray(); + + //read + is = new ByteArrayInputStream(compressed); + os = new ByteArrayOutputStream(); + compressor.uncompress(is, os); + final ByteArrayInputStream bais = new ByteArrayInputStream(os.toByteArray()); + final MemoryCacheImageInputStream resultIis = new MemoryCacheImageInputStream(bais); + resultIis.setByteOrder(byteOrder); + final short[] uncompressed = new short[input.length]; + resultIis.readFully(uncompressed, 0, uncompressed.length); + assertThat(input, is(equalTo(uncompressed))); + } + @Test public void read_BloscCompressor_DefaultAvailable() throws IOException { final Compressor compressor = CompressorFactory.create("blosc");