From e5c6832ec0acd03a041b658b58682afa47669894 Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Mon, 13 Jan 2020 21:03:40 +0100 Subject: [PATCH] #490: Allow writing more TIFF fields. --- .../imageio/metadata/tiff/TIFF.java | 1 + .../imageio/plugins/tiff/TIFFImageWriter.java | 274 +++++++++--------- .../plugins/tiff/TIFFImageWriterTest.java | 6 +- 3 files changed, 143 insertions(+), 138 deletions(-) diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java index dd85dc88..19171d88 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/TIFF.java @@ -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; diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java index d5f71b39..78189058 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriter.java @@ -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 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 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()); } diff --git a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java index 164ad217..9f971437 100644 --- a/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java +++ b/imageio/imageio-tiff/src/test/java/com/twelvemonkeys/imageio/plugins/tiff/TIFFImageWriterTest.java @@ -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)) {