blob: e86139e6f81a49ba4f914f27cbafcbfaa2633af2 [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.commons.imaging.formats.tiff;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteOrder;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.imaging.FormatCompliance;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.bytesource.ByteSource;
import org.apache.commons.imaging.formats.tiff.constants.TiffPlanarConfiguration;
import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
import org.apache.commons.imaging.formats.tiff.write.TiffImageWriterLossy;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
/**
* Performs a test in which a TIFF file with the special-purpose floating-point sample type is used to store data to a file. The file is then read to see if it
* matches the original values.
* <p>
* At this time, Commons Imaging does not fully implement the floating-point specification. Currently, this class only tests the use of uncompressed floating
* point values in the Strips format. The Tiles format is not exercised.
*/
public class TiffFloatingPointMultivariableTest extends TiffBaseTest {
@TempDir
Path tempDir;
int width = 48;
int height = 23;
int samplesPerPixel = 2;
float f0 = 0.0F;
float f1 = 1.0F;
float[] fSample = new float[width * height * samplesPerPixel];
public TiffFloatingPointMultivariableTest() {
for (int iPlane = 0; iPlane < 2; iPlane++) {
final int pOffset = iPlane * width * height;
for (int iRow = 0; iRow < height; iRow++) {
for (int iCol = 0; iCol < width; iCol++) {
final int index = pOffset + iRow * width + iCol;
fSample[index] = index;
}
}
}
}
private void applyTilePredictor(final int nRowsInBlock, final int nColsInBlock, final byte[] bytes) {
// The floating-point horizonal predictor breaks the samples into
// separate sets of bytes. The first set contains the high-order bytes.
// The second the second-highest order bytes, etc. Once the bytes are
// separated, differencing is applied. This treatment improves the
// statistical predictability of the data. By doing so, it improves
// its compressibility.
// More extensive discussions of this technique are given in the
// Javadoc for the TIFF-specific ImageDataReader class.
final byte[] b = new byte[bytes.length];
final int bytesInRow = nColsInBlock * 4;
for (int iPlane = 0; iPlane < samplesPerPixel; iPlane++) {
// separate out the groups of bytes
final int planarByteOffset = iPlane * nRowsInBlock * nColsInBlock * 4;
for (int i = 0; i < nRowsInBlock; i++) {
final int aOffset = planarByteOffset + i * bytesInRow;
final int bOffset = aOffset + nColsInBlock;
final int cOffset = bOffset + nColsInBlock;
final int dOffset = cOffset + nColsInBlock;
for (int j = 0; j < nColsInBlock; j++) {
b[aOffset + j] = bytes[aOffset + j * 4];
b[bOffset + j] = bytes[aOffset + j * 4 + 1];
b[cOffset + j] = bytes[aOffset + j * 4 + 2];
b[dOffset + j] = bytes[aOffset + j * 4 + 3];
}
// apply differencing
for (int j = bytesInRow - 1; j > 0; j--) {
b[aOffset + j] -= b[aOffset + j - 1];
}
}
}
// copy the results back over the input byte array
System.arraycopy(b, 0, bytes, 0, bytes.length);
}
/**
* Gets the bytes for output for a 32 bit floating point format. Note that this method operates over "blocks" of data which may represent either TIFF Strips
* or Tiles. When processing strips, there is always one column of blocks and each strip is exactly the full width of the image. When processing tiles,
* there may be one or more columns of blocks and the block coverage may extend beyond both the last row and last column.
*
* @param f an array of the grid of output values in row major order
* @param width the width of the overall image
* @param height the height of the overall image
* @param nRowsInBlock the number of rows in the Strip or Tile
* @param nColsInBlock the number of columns in the Strip or Tile
* @param byteOrder little-endian or big-endian
* @return a valid array of equally sized array.
*/
private byte[][] getBytesForOutput32(final int nRowsInBlock, final int nColsInBlock, final ByteOrder byteOrder, final boolean useTiles,
final TiffPlanarConfiguration planarConfiguration) {
final int nColsOfBlocks = (width + nColsInBlock - 1) / nColsInBlock;
final int nRowsOfBlocks = (height + nRowsInBlock + 1) / nRowsInBlock;
final int bytesPerPixel = 4 * samplesPerPixel;
final int nBlocks = nRowsOfBlocks * nColsOfBlocks;
final int nBytesInBlock = bytesPerPixel * nRowsInBlock * nColsInBlock;
final byte[][] blocks = new byte[nBlocks][nBytesInBlock];
if (planarConfiguration == TiffPlanarConfiguration.CHUNKY) {
for (int i = 0; i < height; i++) {
final int blockRow = i / nRowsInBlock;
final int rowInBlock = i - blockRow * nRowsInBlock;
for (int j = 0; j < width; j++) {
final int blockCol = j / nColsInBlock;
final int colInBlock = j - blockCol * nColsInBlock;
final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol]; // reference to relevant block
for (int k = 0; k < 2; k++) {
final float sValue = fSample[k * width * height + i * width + j];
final int sample = Float.floatToRawIntBits(sValue);
final int offset = (rowInBlock * nColsInBlock + colInBlock) * 8 + k * 4;
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
b[offset] = (byte) (sample & 0xff);
b[offset + 1] = (byte) (sample >> 8 & 0xff);
b[offset + 2] = (byte) (sample >> 16 & 0xff);
b[offset + 3] = (byte) (sample >> 24 & 0xff);
} else {
b[offset] = (byte) (sample >> 24 & 0xff);
b[offset + 1] = (byte) (sample >> 16 & 0xff);
b[offset + 2] = (byte) (sample >> 8 & 0xff);
b[offset + 3] = (byte) (sample & 0xff);
}
}
}
}
} else {
for (int i = 0; i < height; i++) {
final int blockRow = i / nRowsInBlock;
final int rowInBlock = i - blockRow * nRowsInBlock;
int blockPlanarOffset = nRowsInBlock * nColsInBlock;
if (!useTiles && (blockRow + 1) * nRowsInBlock > height) {
// For TIFF files using the Strip format, the convention
// is to not include any extra padding in the data. So if the
// height of the image is not evenly divided by the number
// of rows per strip, an adjustmnet is made to the size of the block.
// However, the TIFF specification calls for tiles to always be padded.
final int nRowsAdjusted = height - blockRow * nRowsInBlock;
blockPlanarOffset = nRowsAdjusted * nColsInBlock;
}
for (int j = 0; j < width; j++) {
final int blockCol = j / nColsInBlock;
final int colInBlock = j - blockCol * nColsInBlock;
final byte[] b = blocks[blockRow * nColsOfBlocks + blockCol]; // reference to relevant block
for (int k = 0; k < 2; k++) {
final float sValue = fSample[k * width * height + i * width + j];
final int sample = Float.floatToRawIntBits(sValue);
final int offset = (k * blockPlanarOffset + rowInBlock * nColsInBlock + colInBlock) * 4;
if (byteOrder == ByteOrder.LITTLE_ENDIAN) {
b[offset] = (byte) (sample & 0xff);
b[offset + 1] = (byte) (sample >> 8 & 0xff);
b[offset + 2] = (byte) (sample >> 16 & 0xff);
b[offset + 3] = (byte) (sample >> 24 & 0xff);
} else {
b[offset] = (byte) (sample >> 24 & 0xff);
b[offset + 1] = (byte) (sample >> 16 & 0xff);
b[offset + 2] = (byte) (sample >> 8 & 0xff);
b[offset + 3] = (byte) (sample & 0xff);
}
}
}
}
}
return blocks;
}
@Test
public void test() throws Exception {
// we set up the 32 and 64 bit test cases. At this time,
// the Tile format is not supported for floating-point samples by the
// TIFF datareaders classes. So that format is not yet exercised.
// Note also that the compressed floating-point with predictor=3
// is processed in other tests, but not here.
final List<File> testFiles = new ArrayList<>();
testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, false, false, TiffPlanarConfiguration.CHUNKY));
testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, false, false, TiffPlanarConfiguration.CHUNKY));
testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, true, false, TiffPlanarConfiguration.CHUNKY));
testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, true, false, TiffPlanarConfiguration.CHUNKY));
testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, false, false, TiffPlanarConfiguration.PLANAR));
testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, false, false, TiffPlanarConfiguration.PLANAR));
testFiles.add(writeFile(ByteOrder.LITTLE_ENDIAN, true, false, TiffPlanarConfiguration.PLANAR));
testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, true, false, TiffPlanarConfiguration.PLANAR));
// To exercise the horizontal-differencing-predictor logic, we include a writer that will
// reorganize the bytes into the form used by the floating-pont horizontal predictor.
// This test does not apply data compression, but it does apply the predictor.
// Note that although the TIFF predictor does not require big-endian formats, per se,
// the test logic implemented here does.
testFiles.add(writeFile(ByteOrder.BIG_ENDIAN, true, true, TiffPlanarConfiguration.PLANAR));
for (final File testFile : testFiles) {
final String name = testFile.getName();
final ByteSource byteSource = ByteSource.file(testFile);
final TiffReader tiffReader = new TiffReader(true);
final TiffContents contents = tiffReader.readDirectories(byteSource, true, // indicates that application should read image data, if present
FormatCompliance.getDefault());
final TiffDirectory directory = contents.directories.get(0);
final TiffRasterData raster = directory.getRasterData(new TiffImagingParameters());
assertNotNull(raster, "Failed to get raster from " + name);
assertEquals(2, raster.getSamplesPerPixel(), "Invalid samples per pixel in " + name);
for (int iPlane = 0; iPlane < 2; iPlane++) {
final int pOffset = iPlane * width * height;
for (int iRow = 0; iRow < height; iRow++) {
for (int iCol = 0; iCol < width; iCol++) {
final int index = pOffset + iRow * width + iCol;
final float tValue = fSample[index];
final float rValue = raster.getValue(iCol, iRow, iPlane);
assertEquals(tValue, rValue, "Failed at index x=" + iCol + ", y=" + iRow + ", iPlane=" + iPlane);
}
}
}
}
}
private File writeFile(final ByteOrder byteOrder, final boolean useTiles, final boolean usePredictorForTiles,
final TiffPlanarConfiguration planarConfiguration) throws IOException, ImagingException {
final String name = String.format("FpMultiVarRoundTrip_%s_%s%s.tiff", planarConfiguration == TiffPlanarConfiguration.CHUNKY ? "Chunky" : "Planar",
useTiles ? "Tiles" : "Strips", usePredictorForTiles ? "_Predictor" : "");
final File outputFile = new File(tempDir.toFile(), name);
final int bytesPerSample = 4 * samplesPerPixel;
final int bitsPerSample = 8 * bytesPerSample;
int nRowsInBlock;
int nColsInBlock;
int nBytesInBlock;
if (useTiles) {
// Define the tiles so that they will not evenly subdivide
// the image. This will allow the test to evaluate how the
// data reader processes tiles that are only partially used.
nRowsInBlock = 12;
nColsInBlock = 20;
} else {
// Define the strips so that they will not evenly subdivide
// the image. This will allow the test to evaluate how the
// data reader processes strips that are only partially used.
nRowsInBlock = 2;
nColsInBlock = width;
}
nBytesInBlock = nRowsInBlock * nColsInBlock * bytesPerSample;
byte[][] blocks;
blocks = this.getBytesForOutput32(nRowsInBlock, nColsInBlock, byteOrder, useTiles, planarConfiguration);
final TiffOutputSet outputSet = new TiffOutputSet(byteOrder);
final TiffOutputDirectory outDir = outputSet.addRootDirectory();
outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_WIDTH, width);
outDir.add(TiffTagConstants.TIFF_TAG_IMAGE_LENGTH, height);
outDir.add(TiffTagConstants.TIFF_TAG_SAMPLE_FORMAT, (short) TiffTagConstants.SAMPLE_FORMAT_VALUE_IEEE_FLOATING_POINT);
outDir.add(TiffTagConstants.TIFF_TAG_SAMPLES_PER_PIXEL, (short) samplesPerPixel);
outDir.add(TiffTagConstants.TIFF_TAG_BITS_PER_SAMPLE, (short) bitsPerSample);
outDir.add(TiffTagConstants.TIFF_TAG_PHOTOMETRIC_INTERPRETATION, (short) TiffTagConstants.PHOTOMETRIC_INTERPRETATION_VALUE_BLACK_IS_ZERO);
outDir.add(TiffTagConstants.TIFF_TAG_COMPRESSION, (short) TiffTagConstants.COMPRESSION_VALUE_UNCOMPRESSED);
if (useTiles && usePredictorForTiles) {
outDir.add(TiffTagConstants.TIFF_TAG_PREDICTOR, (short) TiffTagConstants.PREDICTOR_VALUE_FLOATING_POINT_DIFFERENCING);
for (final byte[] block : blocks) {
applyTilePredictor(nRowsInBlock, nColsInBlock, block);
}
}
if (planarConfiguration == TiffPlanarConfiguration.CHUNKY) {
outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_CHUNKY);
} else {
outDir.add(TiffTagConstants.TIFF_TAG_PLANAR_CONFIGURATION, (short) TiffTagConstants.PLANAR_CONFIGURATION_VALUE_PLANAR);
}
if (useTiles) {
outDir.add(TiffTagConstants.TIFF_TAG_TILE_WIDTH, nColsInBlock);
outDir.add(TiffTagConstants.TIFF_TAG_TILE_LENGTH, nRowsInBlock);
outDir.add(TiffTagConstants.TIFF_TAG_TILE_BYTE_COUNTS, nBytesInBlock);
} else {
outDir.add(TiffTagConstants.TIFF_TAG_ROWS_PER_STRIP, nRowsInBlock);
outDir.add(TiffTagConstants.TIFF_TAG_STRIP_BYTE_COUNTS, nBytesInBlock);
}
final AbstractTiffElement.DataElement[] imageData = new AbstractTiffElement.DataElement[blocks.length];
for (int i = 0; i < blocks.length; i++) {
imageData[i] = new AbstractTiffImageData.Data(0, blocks[i].length, blocks[i]);
}
AbstractTiffImageData abstractTiffImageData;
if (useTiles) {
abstractTiffImageData = new AbstractTiffImageData.Tiles(imageData, nColsInBlock, nRowsInBlock);
} else {
abstractTiffImageData = new AbstractTiffImageData.Strips(imageData, nRowsInBlock);
}
outDir.setTiffImageData(abstractTiffImageData);
try (FileOutputStream fos = new FileOutputStream(outputFile);
BufferedOutputStream bos = new BufferedOutputStream(fos)) {
final TiffImageWriterLossy writer = new TiffImageWriterLossy(byteOrder);
writer.write(bos, outputSet);
bos.flush();
}
return outputFile;
}
}