-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor TIFF writing to be more performant and easier to extend in t…
…he future, add BufferedFileChannelOutputStream with some useful file writing enhancements
- Loading branch information
Maximilian Stiede
committed
Oct 29, 2023
1 parent
1715937
commit c8d0b4c
Showing
9 changed files
with
787 additions
and
276 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
37
chunky/src/java/se/llbit/imageformats/tiff/CompressionType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
chunky/src/java/se/llbit/imageformats/tiff/FinalizableBFCOutputStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
Oops, something went wrong.