diff --git a/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java b/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java index 1bd9da2227c..77d0c2b316c 100644 --- a/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java +++ b/components/bio-formats-tools/src/loci/formats/tools/ImageConverter.java @@ -62,6 +62,7 @@ import loci.formats.FileStitcher; import loci.formats.FormatException; import loci.formats.FormatTools; +import loci.formats.ICompressedTileReader; import loci.formats.IFormatReader; import loci.formats.IFormatWriter; import loci.formats.ImageReader; @@ -71,7 +72,9 @@ import loci.formats.MetadataTools; import loci.formats.MinMaxCalculator; import loci.formats.MissingLibraryException; +import loci.formats.codec.Codec; import loci.formats.codec.CodecOptions; +import loci.formats.codec.CompressionType; import loci.formats.codec.JPEG2000CodecOptions; import loci.formats.gui.Index16ColorModel; import loci.formats.in.DynamicMetadataOptions; @@ -558,6 +561,11 @@ public boolean testConvert(IFormatWriter writer, String[] args) reader.setId(in); + if (compression == null && precompressed) { + compression = getReaderCodecName(); + LOGGER.info("Implicitly using compression = {}", compression); + } + if (swapOrder != null) { dimSwapper.swapDimensions(swapOrder); } @@ -759,7 +767,9 @@ else if (writer instanceof ImageWriter) { int writerSeries = series == -1 ? q : 0; writer.setSeries(writerSeries); writer.setResolution(res); + writer.setInterleaved(reader.isInterleaved() && !autoscale); + writer.setValidBitsPerPixel(reader.getBitsPerPixel()); int numImages = writer.canDoStacks() ? reader.getImageCount() : 1; @@ -832,6 +842,12 @@ else if (saveTileWidth > 0 && saveTileHeight > 0) { } } + if (precompressed && FormatTools.canUsePrecompressedTiles(reader, writer, writer.getSeries(), writer.getResolution())) { + if (getReaderCodecName().startsWith("JPEG")) { + writer.setInterleaved(true); + } + } + int outputIndex = 0; if (nextOutputIndex.containsKey(outputName)) { outputIndex = nextOutputIndex.get(outputName); @@ -1260,19 +1276,21 @@ private boolean isTiledWriter(IFormatWriter writer, String outputFile) private boolean doTileConversion(IFormatWriter writer, String outputFile) throws FormatException { - if (writer instanceof DicomWriter || - (writer instanceof ImageWriter && ((ImageWriter) writer).getWriter(outputFile) instanceof DicomWriter)) + // if we asked to try a precompressed conversion, + // then the writer's tile sizes will have been set automatically + // according to the input data + // the conversion must then be performed tile-wise to match the tile sizes, + // even if precompression doesn't end up being possible + if (precompressed) { + return true; + } + // tile size has already been set in the writer, + // so tile-wise conversion should be performed + // independent of image size + if ((writer.getTileSizeX() > 0 && writer.getTileSizeX() < width) || + (writer.getTileSizeY() > 0 && writer.getTileSizeY() < height)) { - // if we asked to try a precompressed conversion, - // then the writer's tile sizes will have been set automatically - // according to the input data - // the conversion must then be performed tile-wise to match the tile sizes, - // even if precompression doesn't end up being possible - if (precompressed) { - return true; - } - MetadataStore r = reader.getMetadataStore(); - return !(r instanceof IPyramidStore) || ((IPyramidStore) r).getResolutionCount(reader.getSeries()) > 1; + return true; } return DataTools.safeMultiply64(width, height) >= DataTools.safeMultiply64(4096, 4096) || saveTileWidth > 0 || saveTileHeight > 0; @@ -1318,6 +1336,18 @@ private void setCodecOptions(IFormatWriter writer) { } } + private String getReaderCodecName() throws FormatException, IOException { + if (reader instanceof ICompressedTileReader) { + ICompressedTileReader r = (ICompressedTileReader) reader; + Codec c = r.getTileCodec(0); + CompressionType type = CompressionType.get(c); + if (type != null) { + return type.getCompression(); + } + } + return null; + } + // -- Main method -- public static void main(String[] args) throws FormatException, IOException { diff --git a/components/formats-api/src/loci/formats/ICompressedTileReader.java b/components/formats-api/src/loci/formats/ICompressedTileReader.java index b90fc44a4d0..ad7baee3b7c 100644 --- a/components/formats-api/src/loci/formats/ICompressedTileReader.java +++ b/components/formats-api/src/loci/formats/ICompressedTileReader.java @@ -66,7 +66,7 @@ default int getTileColumns(int no) { * * @param no plane index * @param x tile X index (indexed from 0, @see getTileColumns(int)) - * @param y tile Y index (indexed frmo 0, @see getTileRows(int)) + * @param y tile Y index (indexed from 0, @see getTileRows(int)) * @return compressed tile bytes */ default byte[] openCompressedBytes(int no, int x, int y) throws FormatException, IOException { @@ -79,7 +79,7 @@ default byte[] openCompressedBytes(int no, int x, int y) throws FormatException, * @param no plane index * @param buf pre-allocated buffer in which to store compressed bytes * @param x tile X index (indexed from 0, @see getTileColumns(int)) - * @param y tile Y index (indexed frmo 0, @see getTileRows(int)) + * @param y tile Y index (indexed from 0, @see getTileRows(int)) * @return compressed tile bytes */ default byte[] openCompressedBytes(int no, byte[] buf, int x, int y) throws FormatException, IOException { @@ -102,7 +102,7 @@ default Codec getTileCodec(int no) throws FormatException, IOException { * * @param no plane index * @param x tile X index (indexed from 0, @see getTileColumns(int)) - * @param y tile Y index (indexed frmo 0, @see getTileRows(int)) + * @param y tile Y index (indexed from 0, @see getTileRows(int)) * @return codec options * @see getTileCodec(int) */ diff --git a/components/formats-bsd/src/loci/formats/codec/CompressionType.java b/components/formats-bsd/src/loci/formats/codec/CompressionType.java index 01b595a7727..de9e8fa0111 100644 --- a/components/formats-bsd/src/loci/formats/codec/CompressionType.java +++ b/components/formats-bsd/src/loci/formats/codec/CompressionType.java @@ -115,5 +115,27 @@ public int getCode() { public String getCompression() { return compression; } - + + /** + * Look up the compression type by Codec instance. + */ + public static CompressionType get(Codec c) { + if (c instanceof ZlibCodec) { + return ZLIB; + } + if (c instanceof LZWCodec) { + return LZW; + } + if (c instanceof JPEGCodec) { + return JPEG; + } + if (c instanceof JPEG2000Codec) { + return J2K; + } + if (c instanceof PassthroughCodec) { + return UNCOMPRESSED; + } + return null; + } + } diff --git a/components/formats-bsd/src/loci/formats/in/DicomReader.java b/components/formats-bsd/src/loci/formats/in/DicomReader.java index 4b6c9afeb24..f4c0a4cf841 100644 --- a/components/formats-bsd/src/loci/formats/in/DicomReader.java +++ b/components/formats-bsd/src/loci/formats/in/DicomReader.java @@ -63,6 +63,7 @@ import loci.formats.codec.JPEG2000Codec; import loci.formats.codec.JPEGCodec; import loci.formats.codec.PackbitsCodec; +import loci.formats.codec.PassthroughCodec; import loci.formats.meta.MetadataStore; import ome.xml.model.primitives.Timestamp; import ome.units.quantity.Length; @@ -145,6 +146,94 @@ public DicomReader() { hasCompanionFiles = true; } + // -- ICompressedTileReader API methods -- + + @Override + public int getTileRows(int no) { + FormatTools.assertId(currentId, true, 1); + + return (int) Math.ceil((double) getSizeY() / originalY); + } + + @Override + public int getTileColumns(int no) { + FormatTools.assertId(currentId, true, 1); + + return (int) Math.ceil((double) getSizeX() / originalY); + } + + @Override + public byte[] openCompressedBytes(int no, int x, int y) throws FormatException, IOException { + FormatTools.assertId(currentId, true, 1); + + // TODO: this will result in a lot of redundant lookups, and should be optimized + int tileWidth = getOptimalTileWidth(); + int tileHeight = getOptimalTileHeight(); + Region boundingBox = new Region(x * tileWidth, y * tileHeight, tileWidth, tileHeight); + + List tiles = getTileList(no, boundingBox, true); + if (tiles == null || tiles.size() == 0) { + throw new FormatException("Could not find valid tile; no=" + no + ", boundingBox=" + boundingBox); + } + DicomTile tile = tiles.get(0); + byte[] buf = new byte[(int) (tile.endOffset - tile.fileOffset)]; + try (RandomAccessInputStream stream = new RandomAccessInputStream(tile.file)) { + if (tile.fileOffset >= stream.length()) { + LOGGER.error("attempted to read beyond end of file ({}, {})", tile.fileOffset, tile.file); + return buf; + } + LOGGER.debug("reading from offset = {}, file = {}", tile.fileOffset, tile.file); + stream.seek(tile.fileOffset); + stream.read(buf, 0, (int) (tile.endOffset - tile.fileOffset)); + } + return buf; + } + + @Override + public byte[] openCompressedBytes(int no, byte[] buf, int x, int y) throws FormatException, IOException { + FormatTools.assertId(currentId, true, 1); + + Region boundingBox = new Region(x * originalX, y * originalY, originalX, originalY); + + List tiles = getTileList(no, boundingBox, true); + if (tiles == null || tiles.size() == 0) { + throw new FormatException("Could not find valid tile; no=" + no + ", x=" + x + ", y=" + y); + } + DicomTile tile = tiles.get(0); + try (RandomAccessInputStream stream = new RandomAccessInputStream(tile.file)) { + if (tile.fileOffset >= stream.length()) { + LOGGER.error("attempted to read beyond end of file ({}, {})", tile.fileOffset, tile.file); + return buf; + } + LOGGER.debug("reading from offset = {}, file = {}", tile.fileOffset, tile.file); + stream.seek(tile.fileOffset); + stream.read(buf, 0, (int) (tile.endOffset - tile.fileOffset)); + } + return buf; + } + + @Override + public Codec getTileCodec(int no) throws FormatException, IOException { + FormatTools.assertId(currentId, true, 1); + + List tiles = getTileList(no, null, true); + if (tiles == null || tiles.size() == 0) { + throw new FormatException("Could not find valid tile; no=" + no); + } + return getTileCodec(tiles.get(0)); + } + + @Override + public CodecOptions getTileCodecOptions(int no, int x, int y) throws FormatException, IOException { + FormatTools.assertId(currentId, true, 1); + + List tiles = getTileList(no, null, true); + if (tiles == null || tiles.size() == 0) { + throw new FormatException("Could not find valid tile; no=" + no + ", x=" + x + ", y=" + y); + } + return getTileCodecOptions(tiles.get(0)); + } + // -- IFormatReader API methods -- /* @see loci.formats.IFormatReader#isThisType(String, boolean) */ @@ -250,7 +339,10 @@ public int fileGroupOption(String id) throws FormatException, IOException { public int getOptimalTileWidth() { FormatTools.assertId(currentId, true, 1); if (tilePositions.containsKey(getCoreIndex())) { - return tilePositions.get(getCoreIndex()).get(0).region.width; + List tile = getTileList(0, null, true); + if (tile != null && tile.size() >= 1) { + return tile.get(0).region.width; + } } if (originalX < getSizeX() && originalX > 0) { return originalX; @@ -262,7 +354,10 @@ public int getOptimalTileWidth() { public int getOptimalTileHeight() { FormatTools.assertId(currentId, true, 1); if (tilePositions.containsKey(getCoreIndex())) { - return tilePositions.get(getCoreIndex()).get(0).region.height; + List tile = getTileList(0, null, true); + if (tile != null && tile.size() >= 1) { + return tile.get(0).region.height; + } } if (originalY < getSizeY() && originalY > 0) { return originalY; @@ -282,33 +377,18 @@ public byte[] openBytes(int no, byte[] buf, int x, int y, int w, int h) int bpp = FormatTools.getBytesPerPixel(getPixelType()); int pixel = bpp * getRGBChannelCount(); Region currentRegion = new Region(x, y, w, h); - int z = getZCTCoords(no)[0]; - int c = getZCTCoords(no)[1]; - - if (!tilePositions.containsKey(getCoreIndex())) { - LOGGER.warn("No tiles for core index = {}", getCoreIndex()); - return buf; - } - // look for any tiles that match the requested tile and plane - List zs = zOffsets.get(getCoreIndex()); - List tiles = tilePositions.get(getCoreIndex()); - for (int t=0; t tiles = getTileList(no, currentRegion, false); + for (DicomTile tile : tiles) { + byte[] tileBuf = new byte[tile.region.width * tile.region.height * pixel]; + Region intersection = tile.region.intersection(currentRegion); + getTile(tile, tileBuf, intersection.x - tile.region.x, intersection.y - tile.region.y, + intersection.width, intersection.height); + + for (int row=0; row getTileList(int no, Region boundingBox, boolean firstTileOnly) { + int z = getZCTCoords(no)[0]; + int c = getZCTCoords(no)[1]; + + List tileList = new ArrayList(); + if (!tilePositions.containsKey(getCoreIndex())) { + LOGGER.warn("No tiles for core index = {}", getCoreIndex()); + return tileList; + } + + // look for any tiles that match the requested tile and plane + List zs = zOffsets.get(getCoreIndex()); + List tiles = tilePositions.get(getCoreIndex()); + for (int t=0; t 1) { @@ -1611,13 +1755,6 @@ else if (pt < b.length - 2) { System.arraycopy(tmp, 0, b, 0, b.length); } - Codec codec = null; - CodecOptions options = new CodecOptions(); - options.littleEndian = isLittleEndian(); - options.interleaved = isInterleaved(); - if (tile.isJPEG) codec = new JPEGCodec(); - else codec = new JPEG2000Codec(); - try { b = codec.decompress(b, options); } diff --git a/components/formats-bsd/src/loci/formats/out/OMETiffWriter.java b/components/formats-bsd/src/loci/formats/out/OMETiffWriter.java index eb4afa8fe55..e0785aeb443 100644 --- a/components/formats-bsd/src/loci/formats/out/OMETiffWriter.java +++ b/components/formats-bsd/src/loci/formats/out/OMETiffWriter.java @@ -199,6 +199,24 @@ public void close() throws IOException { // -- IFormatWriter API methods -- + @Override + public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h) + throws FormatException, IOException + { + super.saveCompressedBytes(no, buf, x, y, w, h); + + int index = no; + while (imageLocations[series][index] != null) { + if (index < imageLocations[series].length - 1) { + index++; + } + else { + break; + } + } + imageLocations[series][index] = currentId; + } + /** * @see loci.formats.IFormatWriter#saveBytes(int, byte[], int, int, int, int) */ diff --git a/components/formats-bsd/src/loci/formats/out/PyramidOMETiffWriter.java b/components/formats-bsd/src/loci/formats/out/PyramidOMETiffWriter.java index a1da832176f..d287493b8ac 100644 --- a/components/formats-bsd/src/loci/formats/out/PyramidOMETiffWriter.java +++ b/components/formats-bsd/src/loci/formats/out/PyramidOMETiffWriter.java @@ -70,6 +70,19 @@ public boolean isThisType(String name) { // -- IFormatWriter API methods -- + protected IFD makeIFD() throws FormatException, IOException { + IFD ifd = super.makeIFD(); + if (getResolution() > 0) { + ifd.put(IFD.NEW_SUBFILE_TYPE, 1); + } + else { + if (!ifd.containsKey(IFD.SUB_IFD)) { + ifd.put(IFD.SUB_IFD, (long) 0); + } + } + return ifd; + } + @Override public void saveBytes(int no, byte[] buf, IFD ifd, int x, int y, int w, int h) throws FormatException, IOException diff --git a/components/formats-bsd/src/loci/formats/out/TiffWriter.java b/components/formats-bsd/src/loci/formats/out/TiffWriter.java index fe285fdf81a..8bc78476238 100644 --- a/components/formats-bsd/src/loci/formats/out/TiffWriter.java +++ b/components/formats-bsd/src/loci/formats/out/TiffWriter.java @@ -39,6 +39,7 @@ import loci.formats.FormatTools; import loci.formats.FormatWriter; import loci.formats.ImageTools; +import loci.formats.codec.Codec; import loci.formats.codec.CompressionType; import loci.formats.gui.AWTImageTools; import loci.formats.meta.MetadataRetrieve; @@ -100,13 +101,10 @@ public class TiffWriter extends FormatWriter { protected int tileSizeY; /** - * Sets the compression code for the specified IFD. - * - * @param ifd The IFD table to handle. + * Get the TIFF compression enum value that corresponds to + * the current compression type. */ - private void formatCompression(IFD ifd) - throws FormatException - { + private TiffCompression getTIFFCompression() { if (compression == null) compression = ""; TiffCompression compressType = TiffCompression.UNCOMPRESSED; if (compression.equals(COMPRESSION_LZW)) { @@ -124,9 +122,22 @@ else if (compression.equals(COMPRESSION_JPEG)) { else if (compression.equals(COMPRESSION_ZLIB)) { compressType = TiffCompression.DEFLATE; } + return compressType; + } + + /** + * Sets the compression code for the specified IFD. + * + * @param ifd The IFD table to handle. + */ + private void formatCompression(IFD ifd) + throws FormatException + { + TiffCompression compressType = getTIFFCompression(); Object v = ifd.get(IFD.COMPRESSION); - if (v == null) + if (v == null) { ifd.put(IFD.COMPRESSION, compressType.getCode()); + } } // -- Constructors -- @@ -149,6 +160,72 @@ public TiffWriter(String format, String[] exts) { isBigTiff = false; } + // -- ICompressedTileWriter API methods -- + + @Override + public Codec getCodec() { + return getTIFFCompression().getCodec(); + } + + @Override + public void saveCompressedBytes(int no, byte[] buf, int x, int y, int w, int h) + throws FormatException, IOException + { + if (!sequential) { + throw new UnsupportedOperationException( + "Sequential tile writing must be enabled to write precompressed tiles"); + } + + LOGGER.debug("saveCompressedBytes(series={}, resolution={}, no={}, x={}, y={})", + series, resolution, no, x, y); + + IFD ifd = makeIFD(); + MetadataRetrieve retrieve = getMetadataRetrieve(); + int type = FormatTools.pixelTypeFromString( + retrieve.getPixelsType(series).toString()); + int index = no; + int currentTileSizeX = getTileSizeX(); + int currentTileSizeY = getTileSizeY(); + + if (x % currentTileSizeX != 0 || y % currentTileSizeY != 0 || + (currentTileSizeX != w && x + w != getSizeX()) || + (currentTileSizeY != h && y + h != getSizeY())) + { + throw new IllegalArgumentException("Compressed tile dimensions must match tile size"); + } + + // This operation is synchronized + synchronized (this) { + // This operation is synchronized against the TIFF saver. + synchronized (tiffSaver) { + index = prepareToWriteImage(no, buf, ifd, x, y, w, h); + if (index == -1) { + return; + } + } + } + + boolean lastPlane = no == getPlaneCount() - 1; + boolean lastSeries = getSeries() == retrieve.getImageCount() - 1; + boolean lastResolution = getResolution() == getResolutionCount() - 1; + + int nChannels = getSamplesPerPixel(); + + tiffSaver.makeValidIFD(ifd, type, nChannels); + tiffSaver.writeImageIFD(ifd, index, new byte[][] {buf}, + nChannels, lastPlane && lastSeries && lastResolution, x, y); + } + + protected IFD makeIFD() throws FormatException, IOException { + IFD ifd = new IFD(); + boolean usingTiling = getTileSizeX() > 0 && getTileSizeY() > 0; + if (usingTiling) { + ifd.put(new Integer(IFD.TILE_WIDTH), new Long(getTileSizeX())); + ifd.put(new Integer(IFD.TILE_LENGTH), new Long(getTileSizeY())); + } + return ifd; + } + // -- FormatWriter API methods -- /* @see loci.formats.FormatWriter#setId(String) */ diff --git a/components/formats-bsd/src/loci/formats/tiff/TiffSaver.java b/components/formats-bsd/src/loci/formats/tiff/TiffSaver.java index 8a683cc1d44..06c01da240b 100644 --- a/components/formats-bsd/src/loci/formats/tiff/TiffSaver.java +++ b/components/formats-bsd/src/loci/formats/tiff/TiffSaver.java @@ -350,15 +350,22 @@ public void writeImage(byte[] buf, IFD ifd, int no, int pixelType, int x, stripOut[strip].write(buf, strip * stripSize, stripSize); } } else { - for (int strip = 0; strip < nStrips - 1; strip++) { - stripOut[strip].write(buf, strip * stripSize, stripSize); - } - // Sigh. Need to pad the last strip. - int pos = (nStrips - 1) * stripSize; - int len = buf.length - pos; - stripOut[nStrips - 1].write(buf, pos, len); - for (int n = len; n < stripSize; n++) { - stripOut[nStrips - 1].writeByte(0); + int effectiveStrips = !interleaved ? nStrips / nChannels : nStrips; + int planarChannels = !interleaved ? nChannels : 1; + int totalBytesPerChannel = buf.length / planarChannels; + for (int p=0; p