#490: Allow writing more TIFF fields.

This commit is contained in:
Harald Kuhr 2020-01-13 21:03:40 +01:00
parent 167686bdea
commit e5c6832ec0
3 changed files with 143 additions and 138 deletions

View File

@ -144,6 +144,7 @@ public interface TIFF {
int TAG_ROWS_PER_STRIP = 278;
int TAG_STRIP_BYTE_COUNTS = 279;
int TAG_FREE_OFFSETS = 288; // "Not recommended for general interchange."
int TAG_FREE_BYTE_COUNTS = 289;
// "Old-style" JPEG (still used as EXIF thumbnail)
int TAG_JPEG_INTERCHANGE_FORMAT = 513;
int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = 514;

View File

@ -142,13 +142,17 @@ public final class TIFFImageWriter extends ImageWriterBase {
private long writePage(int imageIndex, IIOImage image, ImageWriteParam param, TIFFWriter tiffWriter, long lastIFDPointerOffset)
throws IOException {
RenderedImage renderedImage = image.getRenderedImage();
TIFFImageMetadata metadata = image.getMetadata() != null
? convertImageMetadata(image.getMetadata(), ImageTypeSpecifier.createFromRenderedImage(renderedImage), param)
: getDefaultImageMetadata(ImageTypeSpecifier.createFromRenderedImage(renderedImage), param);
ColorModel colorModel = renderedImage.getColorModel();
SampleModel sampleModel = renderedImage.getSampleModel();
// Can't use createFromRenderedImage in this case, as it does not consider palette for TYPE_BYTE_BINARY...
// TODO: Consider writing workaround in ImageTypeSpecifiers
ImageTypeSpecifier spec = new ImageTypeSpecifier(renderedImage);
// TODO: Handle case where convertImageMetadata returns null, due to unknown metadata format, or reconsider if that's a valid case...
TIFFImageMetadata metadata = image.getMetadata() != null
? convertImageMetadata(image.getMetadata(), spec, param)
: getDefaultImageMetadata(spec, param);
int numBands = sampleModel.getNumBands();
int pixelSize = computePixelSize(sampleModel);
@ -170,145 +174,29 @@ public final class TIFFImageWriter extends ImageWriterBase {
throw new IllegalArgumentException("Unknown bit/bandOffsets for sample model: " + sampleModel);
}
// TODO: There shouldn't be necessary to create a separate map here, this should be handled in the
// convertImageMetadata/getDefaultImageMetadata methods....
Map<Integer, Entry> entries = new LinkedHashMap<>();
// Copy metadata to output
Directory metadataIFD = metadata.getIFD();
for (Entry entry : metadataIFD) {
entries.put((Integer) entry.getIdentifier(), entry);
}
entries.put(TIFF.TAG_IMAGE_WIDTH, new TIFFEntry(TIFF.TAG_IMAGE_WIDTH, renderedImage.getWidth()));
entries.put(TIFF.TAG_IMAGE_HEIGHT, new TIFFEntry(TIFF.TAG_IMAGE_HEIGHT, renderedImage.getHeight()));
entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
// If numComponents > numColorComponents, write ExtraSamples
if (numBands > colorModel.getNumColorComponents()) {
// TODO: Write per component > numColorComponents
if (colorModel.hasAlpha()) {
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
}
else {
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
}
}
// Write compression field from param or metadata
int compression;
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
&& image.getMetadata() != null && metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION) != null) {
compression = ((Number) metadata.getIFD().getEntryById(TIFF.TAG_COMPRESSION).getValue()).intValue();
}
else {
compression = TIFFImageWriteParam.getCompressionType(param);
}
entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
// TODO: Let param/metadata control predictor
// TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
switch (compression) {
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
case TIFFExtension.COMPRESSION_LZW:
if (pixelSize >= 8) {
entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
}
break;
case TIFFExtension.COMPRESSION_CCITT_T4:
Entry group3options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP3OPTIONS);
if (group3options == null) {
group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING);
}
entries.put(TIFF.TAG_GROUP3OPTIONS, group3options);
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
Entry group4options = metadata.getIFD().getEntryById(TIFF.TAG_GROUP4OPTIONS);
if (group4options == null) {
group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L);
}
entries.put(TIFF.TAG_GROUP4OPTIONS, group4options);
break;
default:
}
int photometric = getPhotometricInterpretation(colorModel, compression);
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, photometric));
if (photometric == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
// TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
// We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
}
else {
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands));
// Note: Assuming sRGB to be the default RGB interpretation
ColorSpace colorSpace = colorModel.getColorSpace();
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
}
}
// Default sample format SAMPLEFORMAT_UINT need not be written
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
// TODO: Float values!
// TODO: Again, this should be handled in the metadata conversion....
// Get Software from metadata, or use default
Entry software = metadata.getIFD().getEntryById(TIFF.TAG_SOFTWARE);
entries.put(TIFF.TAG_SOFTWARE, software != null ? software : new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
// Copy metadata to output
int[] copyTags = {
TIFF.TAG_ORIENTATION,
TIFF.TAG_DATE_TIME,
TIFF.TAG_DOCUMENT_NAME,
TIFF.TAG_IMAGE_DESCRIPTION,
TIFF.TAG_MAKE,
TIFF.TAG_MODEL,
TIFF.TAG_PAGE_NAME,
TIFF.TAG_PAGE_NUMBER,
TIFF.TAG_ARTIST,
TIFF.TAG_HOST_COMPUTER,
TIFF.TAG_COPYRIGHT
};
for (int tagID : copyTags) {
Entry entry = metadata.getIFD().getEntryById(tagID);
if (entry != null) {
entries.put(tagID, entry);
}
}
// Get X/YResolution and ResolutionUnit from metadata if set, otherwise use defaults
// TODO: Add logic here OR in metadata merging, to make sure these 3 values are consistent.
Entry xRes = metadata.getIFD().getEntryById(TIFF.TAG_X_RESOLUTION);
entries.put(TIFF.TAG_X_RESOLUTION, xRes != null ? xRes : new TIFFEntry(TIFF.TAG_X_RESOLUTION, STANDARD_DPI));
Entry yRes = metadata.getIFD().getEntryById(TIFF.TAG_Y_RESOLUTION);
entries.put(TIFF.TAG_Y_RESOLUTION, yRes != null ? yRes : new TIFFEntry(TIFF.TAG_Y_RESOLUTION, STANDARD_DPI));
Entry resUnit = metadata.getIFD().getEntryById(TIFF.TAG_RESOLUTION_UNIT);
entries.put(TIFF.TAG_RESOLUTION_UNIT, resUnit != null ? resUnit : new TIFFEntry(TIFF.TAG_RESOLUTION_UNIT, TIFFBaseline.RESOLUTION_UNIT_DPI));
// TODO: RowsPerStrip - can be entire image (or even 2^32 -1), but it's recommended to write "about 8K bytes" per strip
entries.put(TIFF.TAG_ROWS_PER_STRIP, new TIFFEntry(TIFF.TAG_ROWS_PER_STRIP, renderedImage.getHeight()));
// - StripByteCounts - for no compression, entire image data... (TODO: How to know the byte counts prior to writing data?)
// StripByteCounts - for no compression, entire image data...
entries.put(TIFF.TAG_STRIP_BYTE_COUNTS, new TIFFEntry(TIFF.TAG_STRIP_BYTE_COUNTS, -1)); // Updated later
// - StripOffsets - can be offset to single strip only (TODO: but how large is the IFD data...???)
// StripOffsets - can be offset to single strip only
entries.put(TIFF.TAG_STRIP_OFFSETS, new TIFFEntry(TIFF.TAG_STRIP_OFFSETS, -1)); // Updated later
// TODO: If tiled, write tile indexes etc
// Depending on param.getTilingMode
long nextIFDPointerOffset = -1;
int compression = ((Number) entries.get(TIFF.TAG_COMPRESSION).getValue()).intValue();
if (compression == TIFFBaseline.COMPRESSION_NONE) {
// This implementation, allows semi-streaming-compatible uncompressed TIFFs
long streamPosition = imageOutput.getStreamPosition();
@ -876,12 +764,58 @@ public final class TIFFImageWriter extends ImageWriterBase {
Map<Integer, Entry> entries = new LinkedHashMap<>(ifd != null ? ifd.size() + 10 : 20);
// Set software as default, may be overwritten
entries.put(TIFF.TAG_SOFTWARE, new TIFFEntry(TIFF.TAG_SOFTWARE, "TwelveMonkeys ImageIO TIFF writer " + originatingProvider.getVersion()));
entries.put(TIFF.TAG_ORIENTATION, new TIFFEntry(TIFF.TAG_ORIENTATION, 1)); // (optional)
if (ifd != null) {
for (Entry entry : ifd) {
entries.put((Integer) entry.getIdentifier(), entry);
int tagId = (Integer) entry.getIdentifier();
switch (tagId) {
// Baseline
case TIFF.TAG_SUBFILE_TYPE:
case TIFF.TAG_OLD_SUBFILE_TYPE:
case TIFF.TAG_IMAGE_DESCRIPTION:
case TIFF.TAG_MAKE:
case TIFF.TAG_MODEL:
case TIFF.TAG_ORIENTATION:
case TIFF.TAG_X_RESOLUTION:
case TIFF.TAG_Y_RESOLUTION:
case TIFF.TAG_RESOLUTION_UNIT:
case TIFF.TAG_SOFTWARE:
case TIFF.TAG_DATE_TIME:
case TIFF.TAG_ARTIST:
case TIFF.TAG_HOST_COMPUTER:
case TIFF.TAG_COPYRIGHT:
// Extension
case TIFF.TAG_DOCUMENT_NAME:
case TIFF.TAG_PAGE_NAME:
case TIFF.TAG_X_POSITION:
case TIFF.TAG_Y_POSITION:
case TIFF.TAG_PAGE_NUMBER:
case TIFF.TAG_XMP:
// Private/Custom
case TIFF.TAG_IPTC:
case TIFF.TAG_PHOTOSHOP:
case TIFF.TAG_PHOTOSHOP_IMAGE_SOURCE_DATA:
case TIFF.TAG_PHOTOSHOP_ANNOTATIONS:
case TIFF.TAG_EXIF_IFD:
case TIFF.TAG_GPS_IFD:
case TIFF.TAG_INTEROP_IFD:
entries.put(tagId, entry);
}
}
}
ColorModel colorModel = imageType.getColorModel();
SampleModel sampleModel = imageType.getSampleModel();
int numBands = sampleModel.getNumBands();
int pixelSize = computePixelSize(sampleModel);
entries.put(TIFF.TAG_BITS_PER_SAMPLE, new TIFFEntry(TIFF.TAG_BITS_PER_SAMPLE, asShortArray(sampleModel.getSampleSize())));
// Compression field from param or metadata
int compression;
if ((param == null || param.getCompressionMode() == TIFFImageWriteParam.MODE_COPY_FROM_METADATA)
&& ifd != null && ifd.getEntryById(TIFF.TAG_COMPRESSION) != null) {
@ -890,11 +824,81 @@ public final class TIFFImageWriter extends ImageWriterBase {
else {
compression = TIFFImageWriteParam.getCompressionType(param);
}
entries.put(TIFF.TAG_COMPRESSION, new TIFFEntry(TIFF.TAG_COMPRESSION, compression));
int photometricInterpretation = getPhotometricInterpretation(imageType.getColorModel(), compression);
// TODO: Allow metadata to take precedence?
int photometricInterpretation = getPhotometricInterpretation(colorModel, compression);
entries.put(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, new TIFFEntry(TIFF.TAG_PHOTOMETRIC_INTERPRETATION, TIFF.TYPE_SHORT, photometricInterpretation));
// TODO: Set values from param if != null + combined values...
// If numComponents > numColorComponents, write ExtraSamples
if (numBands > colorModel.getNumColorComponents()) {
// TODO: Write per component > numColorComponents
if (colorModel.hasAlpha()) {
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, colorModel.isAlphaPremultiplied() ? TIFFBaseline.EXTRASAMPLE_ASSOCIATED_ALPHA : TIFFBaseline.EXTRASAMPLE_UNASSOCIATED_ALPHA));
}
else {
entries.put(TIFF.TAG_EXTRA_SAMPLES, new TIFFEntry(TIFF.TAG_EXTRA_SAMPLES, TIFFBaseline.EXTRASAMPLE_UNSPECIFIED));
}
}
switch (compression) {
case TIFFExtension.COMPRESSION_ZLIB:
case TIFFExtension.COMPRESSION_DEFLATE:
case TIFFExtension.COMPRESSION_LZW:
// TODO: Let param/metadata control predictor
// TODO: Depending on param.getCompressionMode(): DISABLED/EXPLICIT/COPY_FROM_METADATA/DEFAULT
if (pixelSize >= 8) {
entries.put(TIFF.TAG_PREDICTOR, new TIFFEntry(TIFF.TAG_PREDICTOR, TIFFExtension.PREDICTOR_HORIZONTAL_DIFFERENCING));
}
break;
case TIFFExtension.COMPRESSION_CCITT_T4:
Entry group3options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP3OPTIONS) : null;
if (group3options == null) {
group3options = new TIFFEntry(TIFF.TAG_GROUP3OPTIONS, (long) TIFFExtension.GROUP3OPT_2DENCODING);
}
entries.put(TIFF.TAG_GROUP3OPTIONS, group3options);
break;
case TIFFExtension.COMPRESSION_CCITT_T6:
Entry group4options = ifd != null ? ifd.getEntryById(TIFF.TAG_GROUP4OPTIONS) : null;
if (group4options == null) {
group4options = new TIFFEntry(TIFF.TAG_GROUP4OPTIONS, 0L);
}
entries.put(TIFF.TAG_GROUP4OPTIONS, group4options);
break;
default:
}
if (photometricInterpretation == TIFFBaseline.PHOTOMETRIC_PALETTE && colorModel instanceof IndexColorModel) {
// TODO: Fix consistency between sampleModel.getSampleSize() and colorModel.getPixelSize()...
// We should be able to support 1, 2, 4 and 8 bits per sample at least, and probably 3, 5, 6 and 7 too
entries.put(TIFF.TAG_COLOR_MAP, new TIFFEntry(TIFF.TAG_COLOR_MAP, createColorMap((IndexColorModel) colorModel, sampleModel.getSampleSize(0))));
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, 1));
}
else {
entries.put(TIFF.TAG_SAMPLES_PER_PIXEL, new TIFFEntry(TIFF.TAG_SAMPLES_PER_PIXEL, numBands));
// Note: Assuming sRGB to be the default RGB interpretation
ColorSpace colorSpace = colorModel.getColorSpace();
if (colorSpace instanceof ICC_ColorSpace && !colorSpace.isCS_sRGB()) {
entries.put(TIFF.TAG_ICC_PROFILE, new TIFFEntry(TIFF.TAG_ICC_PROFILE, ((ICC_ColorSpace) colorSpace).getProfile().getData()));
}
}
// Default sample format SAMPLEFORMAT_UINT need not be written
if (sampleModel.getDataType() == DataBuffer.TYPE_SHORT/* TODO: if isSigned(sampleModel.getDataType) or getSampleFormat(sampleModel) != 0 */) {
entries.put(TIFF.TAG_SAMPLE_FORMAT, new TIFFEntry(TIFF.TAG_SAMPLE_FORMAT, TIFFExtension.SAMPLEFORMAT_INT));
}
// TODO: Float values!
return new TIFFImageMetadata(entries.values());
}

View File

@ -616,7 +616,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
int maxH = Math.min(300, image.getHeight());
for (int y = 0; y < maxH; y++) {
for (int x = 0; x < image.getWidth(); x++) {
assertRGBEquals("Pixel differ: ", orig.getRGB(x, y), image.getRGB(x, y), 0);
assertRGBEquals(String.format("Pixel differ: @%d,%d", x, y), orig.getRGB(x, y), image.getRGB(x, y), 0);
}
}
@ -654,7 +654,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
assumeNotNull(original);
// Write it back, using same compression (copied from metadata)
// Write it back, using deflate compression
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {
@ -718,7 +718,7 @@ public class TIFFImageWriterTest extends ImageWriterAbstractTest {
assumeNotNull(original);
// Write it back, using same compression (copied from metadata)
// Write it back, no compression
FastByteArrayOutputStream buffer = new FastByteArrayOutputStream(32768);
try (ImageOutputStream output = ImageIO.createImageOutputStream(buffer)) {