diff --git a/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV1.java b/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV1.java index f97fbf4581fbbc6af44ce6e67df3cdcd33cedf66..71ab8cf60212df16b7db2bd35b1e85ca6be1d0d2 100644 --- a/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV1.java +++ b/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV1.java @@ -44,7 +44,7 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { */ @Override public boolean validateHeader() { - if (!magicValue.equals(MAGIC_VALUE)) + if (!magicValue.equals(getMagicValue())) return false; if (bitsPerCodebookIndex == 0) @@ -139,26 +139,10 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { builder.append("Header is:\t\t invalid\n"); return; } - builder.append("HeaderVersion\t\t: ").append(VERSION).append('\n'); - builder.append("Magic value\t\t: ").append(magicValue).append('\n'); - builder.append("Quantization type\t: "); - switch (quantizationType) { - case Scalar: - builder.append("Scalar\n"); - break; - case Vector1D: - builder.append("Vector1D\n"); - break; - case Vector2D: - builder.append("Vector2D\n"); - break; - case Vector3D: - builder.append("Vector3D\n"); - break; - case Invalid: - builder.append("INVALID\n"); - break; - } + builder.append("HeaderVersion\t\t: ").append(getHeaderVersion()).append('\n'); + builder.append("Magic value\t\t: ").append(getMagicValue()).append('\n'); + builder.append("Quantization type\t: ").append(quantizationType).append('\n'); + builder.append("Bits per pixel\t\t: ").append(bitsPerCodebookIndex).append('\n'); builder.append("Codebook\t\t: ").append(codebookPerPlane ? "one per plane\n" : "one for all\n"); @@ -176,6 +160,10 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { .append(vectorSizeY).append('x') .append(vectorSizeZ).append('\n'); + printFileSizeInfo(builder, inputFile); + } + + protected void printFileSizeInfo(final StringBuilder builder, final String inputFile) { final long headerSize = getHeaderSize(); final long fileSize = new File(inputFile).length(); final long dataSize = fileSize - headerSize; @@ -190,7 +178,7 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { builder.append("Data size\t\t: "); Utils.prettyPrintFileSize(builder, dataSize).append(correctFileSize ? "(correct)\n" : "(INVALID)\n"); - final long pixelCount = imageSizeX * imageSizeY * imageSizeZ; + final long pixelCount = (long) imageSizeX * imageSizeY * imageSizeZ; final long uncompressedSize = 2 * pixelCount; // We assert 16 bit (2 byte) pixel. final double compressionRatio = (double) fileSize / (double) uncompressedSize; builder.append(String.format("Compression ratio\t: %.4f\n", compressionRatio)); @@ -201,7 +189,7 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { builder.append("\n=== Input file is ").append(correctFileSize ? "VALID" : "INVALID").append(" ===\n"); } - private long calculateDataSizeForSq() { + protected long calculateDataSizeForSq() { final int LONG_BYTES = 8; // Quantization value count. final int codebookSize = (int) Math.pow(2, bitsPerCodebookIndex); @@ -218,7 +206,7 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { return (codebookDataSize + totalPlaneDataSize); } - private long calculateDataSizeForVq() { + protected long calculateDataSizeForVq() { final int LONG_BYTES = 8; // Vector count in codebook final int codebookSize = (int) Math.pow(2, bitsPerCodebookIndex); @@ -252,7 +240,7 @@ public class QCMPFileHeaderV1 implements IFileHeader, Cloneable { @Override public String getMagicValue() { - return magicValue; + return MAGIC_VALUE; } //endregion diff --git a/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV2.java b/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV2.java new file mode 100644 index 0000000000000000000000000000000000000000..63aaf9c34a5e487862322dcf8510580e8770e742 --- /dev/null +++ b/src/main/java/cz/it4i/qcmp/fileformat/QCMPFileHeaderV2.java @@ -0,0 +1,196 @@ +package cz.it4i.qcmp.fileformat; + +import cz.it4i.qcmp.U16; +import cz.it4i.qcmp.compression.VQImageCompressor; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +public class QCMPFileHeaderV2 extends QCMPFileHeaderV1 { + //region Constants + private static final int VERSION = 2; + private static final int BASE_QCMP_HEADER_SIZE = 50; + public static final String MAGIC_VALUE = "QCMPFLV2"; + //endregion + + //region Header fields + private int channelCount; + private int timepointCount; + private int metadataSize; + private byte[] metadata; + //endregion + + //region IFileHeader implementation + + /** + * Validate that all header values are in their valid range. + * + * @return True if this is valid QCMPFILE header. + */ + @Override + public boolean validateHeader() { + if (!super.validateHeader()) + return false; + + if (!U16.isInRange(channelCount)) + return false; + if (!U16.isInRange(timepointCount)) + return false; + + return metadataSize >= 0; + } + + @Override + public void writeToStream(final DataOutputStream outputStream) throws IOException { + // TODO + } + + @Override + public void readFromStream(final DataInputStream inputStream) throws IOException { + // TODO + } + + @Override + public int getHeaderVersion() { + return VERSION; + } + + @Override + public void report(final StringBuilder builder, final String inputFile) { + if (!validateHeader()) { + builder.append("Header is:\t\t invalid\n"); + return; + } + builder.append("HeaderVersion\t\t: ").append(getHeaderVersion()).append('\n'); + builder.append("Magic value\t\t: ").append(getMagicValue()).append('\n'); + builder.append("Quantization type\t: ").append(quantizationType).append('\n'); + builder.append("Bits per pixel\t\t: ").append(bitsPerCodebookIndex).append('\n'); + builder.append("Codebook\t\t: ").append(codebookPerPlane ? "one per plane\n" : "one for all\n"); + + final int codebookSize = (int) Math.pow(2, bitsPerCodebookIndex); + builder.append("Codebook size\t\t: ").append(codebookSize).append('\n'); + + builder.append("Image stack size\t: ") + .append(imageSizeX).append('x') + .append(imageSizeY).append('x') + .append(imageSizeZ).append('x') + .append(channelCount).append('x') + .append(timepointCount).append(" [XxYxZxCxT]").append('\n'); + + builder.append("Quantization vector\t: ") + .append(vectorSizeX).append('x') + .append(vectorSizeY).append('x') + .append(vectorSizeZ).append('\n'); + + builder.append("MetadataSize\t: ").append(metadataSize).append('\n'); + + printFileSizeInfo(builder, inputFile); + } + + @Override + protected long calculateDataSizeForSq() { + // TODO(Moravec): Fix this calculation. Size of the huffman tree will be added to chunk size. + final int LONG_BYTES = 8; + // Quantization value count. + final int codebookSize = (int) Math.pow(2, bitsPerCodebookIndex); + + // Total codebook size in bytes. Also symbol frequencies for Huffman. + final long codebookDataSize = ((2 * codebookSize) + (LONG_BYTES * codebookSize)) * (codebookPerPlane ? imageSizeZ : 1); + + // Indices are encoded using huffman. Plane data size is written in the header. + long totalPlaneDataSize = 0; + for (final long planeDataSize : planeDataSizes) { + totalPlaneDataSize += planeDataSize; + } + + return (codebookDataSize + totalPlaneDataSize); + } + + @Override + protected long calculateDataSizeForVq() { + // TODO(Moravec): Fix this calculation. Size of the huffman tree will be added to chunk size. + final int LONG_BYTES = 8; + // Vector count in codebook + final int codebookSize = (int) Math.pow(2, bitsPerCodebookIndex); + + // Single vector size in bytes. + final int vectorDataSize = 2 * vectorSizeX * vectorSizeY * vectorSizeZ; + + // Total codebook size in bytes. + final long codebookDataSize = ((codebookSize * vectorDataSize) + (codebookSize * LONG_BYTES)) * (codebookPerPlane ? imageSizeZ : 1); + + // Indices are encoded using huffman. Plane data size is written in the header. + long totalPlaneDataSize = 0; + for (final long planeDataSize : planeDataSizes) { + totalPlaneDataSize += planeDataSize; + } + return (codebookDataSize + totalPlaneDataSize); + } + + @Override + public String getMagicValue() { + return MAGIC_VALUE; + } + + //endregion + + //region Cloneable implementation + @Override + protected Object clone() throws CloneNotSupportedException { + return super.clone(); + } + + public QCMPFileHeaderV2 copyOf() { + try { + return (QCMPFileHeaderV2) this.clone(); + } catch (final CloneNotSupportedException e) { + return null; + } + } + //endregion + + //region Getters and Setters + + + public int getChannelCount() { + return channelCount; + } + + public void setChannelCount(final int channelCount) { + this.channelCount = channelCount; + } + + public int getTimepointCount() { + return timepointCount; + } + + public void setTimepointCount(final int timepointCount) { + this.timepointCount = timepointCount; + } + + public int getMetadataSize() { + return metadataSize; + } + + public void setMetadataSize(final int metadataSize) { + this.metadataSize = metadataSize; + } + + public byte[] getMetadata() { + return metadata; + } + + public void setMetadata(final byte[] metadata) { + this.metadata = metadata; + } + + public long getHeaderSize() { + final int chunkCount = (quantizationType != QuantizationType.Vector3D) + ? imageSizeZ + : VQImageCompressor.calculateVoxelLayerCount(imageSizeZ, vectorSizeZ); + + return BASE_QCMP_HEADER_SIZE + metadataSize + (chunkCount * 4L); + } + //endregion +} \ No newline at end of file