Skip to content

Commit

Permalink
refactor TIFF writing to be more performant and easier to extend in t…
Browse files Browse the repository at this point in the history
…he future,

add BufferedFileChannelOutputStream with some useful file writing enhancements
  • Loading branch information
Maximilian Stiede committed Oct 29, 2023
1 parent 1715937 commit c8d0b4c
Show file tree
Hide file tree
Showing 9 changed files with 787 additions and 276 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
*/
package se.llbit.chunky.renderer.export;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import se.llbit.chunky.renderer.scene.Scene;
import se.llbit.imageformats.tiff.CompressionType;
import se.llbit.imageformats.tiff.TiffFileWriter;
import se.llbit.util.TaskTracker;

Expand Down Expand Up @@ -50,8 +52,12 @@ public boolean isTransparencySupported() {

@Override
public void write(OutputStream out, Scene scene, TaskTracker taskTracker) throws IOException {
assert(out instanceof FileOutputStream);
try (TaskTracker.Task task = taskTracker.task("Writing TIFF");
TiffFileWriter writer = new TiffFileWriter(out)) {
TiffFileWriter writer = new TiffFileWriter(
((FileOutputStream) out).getChannel(),
CompressionType.DEFLATE
)) {
writer.export(scene, task);
}
}
Expand Down
50 changes: 50 additions & 0 deletions chunky/src/java/se/llbit/imageformats/tiff/BasicIFD.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package se.llbit.imageformats.tiff;

import java.io.IOException;

public class BasicIFD extends ImageFileDirectory {
final CompressionType compressionType;

public BasicIFD(
int width, int height,
CompressionType compressionType
) throws IOException {
this.compressionType = compressionType;

// RGB full color
addTag(IFDTag.TAG_PHOTOMETRIC_INTERPRETATION, (short) 2);
// Store pixel components contiguously [RGBRGBRGB...]
addTag(IFDTag.TAG_PLANAR_CONFIGURATION, (short) 1);

assert (width <= Short.MAX_VALUE);
addTag(IFDTag.TAG_IMAGE_WIDTH, (short) width);
assert (height <= Short.MAX_VALUE);
addTag(IFDTag.TAG_IMAGE_HEIGHT, (short) height);
// The 0th row represents the visual top of the image, and the 0th column represents the visual left-hand side.
addTag(IFDTag.TAG_ORIENTATION, (short) 1);

// No compression, but pack data into bytes as tightly as possible, leaving no unused
// bits (except at the end of a row). The component values are stored as an array of
// type BYTE. Each scan line (row) is padded to the next BYTE boundary.
addTag(IFDTag.TAG_COMPRESSION_TYPE, compressionType.id);

// Image does not have a physical size
addTag(IFDTag.TAG_RESOLUTION_UNIT, (short) 1); // not an absolute unit
addMultiTag(IFDTag.TAG_X_RESOLUTION, new int[]{1, 1});
addMultiTag(IFDTag.TAG_Y_RESOLUTION, new int[]{1, 1});

// "Compressed or uncompressed image data can be stored almost anywhere in a
// TIFF file. TIFF also supports breaking an image into separate strips for increased
// editing flexibility and efficient I/O buffering."
// We will use exactly 1 strip, therefore the relevant tags have only 1 entry with all rows in 1 strip.
addTag(IFDTag.TAG_ROWS_PER_STRIP, height);
}

@Override
void writePixelData(
FinalizableBFCOutputStream out,
PixelDataWriter writer
) throws IOException {
compressionType.writePixelData(out, writer);
}
}
37 changes: 37 additions & 0 deletions chunky/src/java/se/llbit/imageformats/tiff/CompressionType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package se.llbit.imageformats.tiff;

import java.io.DataOutputStream;
import java.io.IOException;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

public enum CompressionType {
NONE(0x0001),
DEFLATE(0x80B2);

final short id;

CompressionType(int id) {
this.id = (short) id;
}

void writePixelData(
FinalizableBFCOutputStream out,
ImageFileDirectory.PixelDataWriter writer
) throws IOException {
switch (this) {
case NONE:
writer.writePixelData(out);
out.flush();
break;

case DEFLATE:
Deflater deflater = new Deflater(Deflater.BEST_COMPRESSION, false);
DeflaterOutputStream deflOut = new DeflaterOutputStream(out, deflater, 16 * 1024, true);
writer.writePixelData(new DataOutputStream(deflOut));
deflOut.finish();
deflater.end();
break;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package se.llbit.imageformats.tiff;

import se.llbit.util.io.BufferedFileChannelOutputStream;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.ArrayDeque;
import java.util.Deque;

class FinalizableBFCOutputStream extends BufferedFileChannelOutputStream {

private static final Deque<UnfinalizedData<?>> finalizationQueue = new ArrayDeque<>();

public FinalizableBFCOutputStream(FileChannel fileChannel) {
super(fileChannel);
}

void ensureAlignment() throws IOException {
if ((position() & 0b1) != 0)
write((byte) 0);
}

UnfinalizedData.Int writeUnfinalizedInt() throws IOException {
return writeUnfinalized(new UnfinalizedData.Int((int) position()), 4);
}
private <T extends UnfinalizedData<?>> T writeUnfinalized(T ud, int byteCount) throws IOException {
finalizationQueue.add(ud);
skip(byteCount);
return ud;
}

@Override
public void close() throws IOException {
for(UnfinalizedData<?> data : finalizationQueue) {
data.write(this);
}
super.close();
}

static abstract class UnfinalizedData<T> {
final long position;
protected T data;

UnfinalizedData(long position) {
this.position = position;
}

public void setData(T data) {
this.data = data;
}

public T getData() {
return data;
}

public void write(FinalizableBFCOutputStream out) throws IOException {
out.position(position);
if(data != null) {
writeData(out);
}
}

abstract void writeData(FinalizableBFCOutputStream out) throws IOException;

static class Int extends UnfinalizedData<Integer> {
Int(long position) {
super(position);
}

@Override
void writeData(FinalizableBFCOutputStream out) throws IOException {
out.writeInt(data);
}
}
}
}
188 changes: 188 additions & 0 deletions chunky/src/java/se/llbit/imageformats/tiff/IFDTag.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package se.llbit.imageformats.tiff;

import java.io.DataOutput;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public abstract class IFDTag<SingleType, ArrayType> {
final TagFieldType type;
final short tagId;

enum TagFieldType {
ASCII(2, 1),
SHORT(3, 2),
LONG(4, 4),
RATIONAL(5, 8);
final short id;
final int byteSize;

TagFieldType(int id, int byteSize) {
this.id = (short) id;
this.byteSize = byteSize;
}
}

IFDTag(TagFieldType type, short tagId) {
this.type = type;
this.tagId = tagId;
}

abstract int write(DataOutput out, SingleType data) throws IOException;

int writeMultiple(DataOutput out, ArrayType data) throws IOException {
throw new UnsupportedOperationException("cannot store multiple data entries");
}

abstract int valueCount(SingleType st, ArrayType at);

/**
* width / columns / pixels per scanline
*/
static final ShortTag TAG_IMAGE_WIDTH = new ShortTag(0x0100);
/**
* height / rows / length / scanline count
*/
static final ShortTag TAG_IMAGE_HEIGHT = new ShortTag(0x0101);
static final ShortTag TAG_BITS_PER_SAMPLE = new ShortTag(0x0102);
static final ShortTag TAG_SAMPLE_FORMAT = new ShortTag(0x0153);

/**
* defines details of subfile using 32 flag bits
*/
static final LongTag TAG_NEW_SUBFILE_TYPE = new LongTag(0x00FE);

static final ShortTag TAG_COMPRESSION_TYPE = new ShortTag(0x0103);
static final ShortTag TAG_PHOTOMETRIC_INTERPRETATION = new ShortTag(0x0106);
static final ShortTag TAG_PLANAR_CONFIGURATION = new ShortTag(0x011C);

/**
* number of rows in each strip (except possibly the last strip)
*/
static final LongTag TAG_ROWS_PER_STRIP = new LongTag(0x0116);
/**
* for each strip, the byte offset of that strip
*/
static final LongTag TAG_STRIP_OFFSETS = new LongTag(0x0111);
/**
* for each strip, the number of bytes in that strip after any compression
*/
static final LongTag TAG_STRIP_BYTE_COUNTS = new LongTag(0x0117);
static final ShortTag TAG_ORIENTATION = new ShortTag(0x0112);
static final ShortTag TAG_SAMPLES_PER_PIXEL = new ShortTag(0x0115);

static final RationalTag TAG_X_RESOLUTION = new RationalTag(0x011A);
static final RationalTag TAG_Y_RESOLUTION = new RationalTag(0x011B);
static final ShortTag TAG_RESOLUTION_UNIT = new ShortTag(0x0128);

static final ASCIITag TAG_SOFTWARE = new ASCIITag(0x0131);
static final ASCIITag TAG_DATETIME = new ASCIITag(0x0132);

/**
* 7-bit ASCII code, 0-terminated
*/
static class ASCIITag extends IFDTag<String, Void> {
ASCIITag(int tagID) {
super(TagFieldType.ASCII, (short) tagID);
}

@Override
int write(DataOutput out, String data) throws IOException {
byte[] strBuf = data.getBytes(StandardCharsets.US_ASCII);
out.write(strBuf);
out.writeByte(0);
return strBuf.length + 1;
}

@Override
int valueCount(String st, Void at) {
return st.getBytes(StandardCharsets.US_ASCII).length + 1;
}
}

/**
* 16-bit unsigned(!) integer
*/
static class ShortTag extends IFDTag<Short, short[]> {
ShortTag(int tagID) {
super(TagFieldType.SHORT, (short) tagID);
}

@Override
int write(DataOutput out, Short data) throws IOException {
out.writeShort(data);
return 2;
}

@Override
int writeMultiple(DataOutput out, short[] data) throws IOException {
for (short s : data) {
out.writeShort(s);
}
return data.length * 2;
}

@Override
int valueCount(Short st, short[] at) {
return at != null ? at.length : 1;
}
}

/**
* 32-bit unsigned(!) integer
*/
static class LongTag extends IFDTag<Integer, int[]> {
LongTag(int tagID) {
super(TagFieldType.LONG, (short) tagID);
}

@Override
int write(DataOutput out, Integer data) throws IOException {
out.writeInt(data);
return 4;
}

@Override
int writeMultiple(DataOutput out, int[] data) throws IOException {
for (int i : data) {
out.writeInt(i);
}
return data.length * 4;
}

@Override
int valueCount(Integer st, int[] at) {
return at != null ? at.length : 1;
}
}

/**
* fraction using:
* - 32-bit unsigned(!) integer numerator
* - 32-bit unsigned(!) integer denominator
*/
static class RationalTag extends IFDTag<Void, int[]> {
RationalTag(int tagID) {
super(TagFieldType.RATIONAL, (short) tagID);
}

@Override
int write(DataOutput out, Void data) {
throw new UnsupportedOperationException("fraction requires numerator denominator pairs");
}

@Override
int writeMultiple(DataOutput out, int[] numeratorDenominatorPairs) throws IOException {
for (int nd : numeratorDenominatorPairs) {
out.writeInt(nd);
}
return numeratorDenominatorPairs.length * 4;
}

@Override
int valueCount(Void st, int[] numeratorDenominatorPairs) {
if (numeratorDenominatorPairs.length % 2 != 0)
throw new IllegalArgumentException("fraction requires pairs of numerators and denominators");
return numeratorDenominatorPairs.length;
}
}
}
Loading

0 comments on commit c8d0b4c

Please sign in to comment.