Search code examples
javametadatatiffjavax.imageiotwelvemonkeys

Add custom metadata to tiff


I want to add some custom metadata to a multipage tiff for further processing steps, like

  • identifier1 = XYZ1
  • identifier2 = XYZ2
  • ...

My idea was to update (see code/TODO below)

  • IIOMetadata streamMetadata [option 1]
  • IIOMetadata imageMetadata [option 2]
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;

public class TiffMetadataExample {

  public static void addMetadata(File tiff, File out, Object metadata2Add)
      throws FileNotFoundException, IOException {
    try (FileInputStream fis = new FileInputStream(tiff);
        FileOutputStream fos = new FileOutputStream(out)) {
      addMetadata(fis, fos, metadata2Add);
    }
  }

  public static void addMetadata(InputStream inputImage, OutputStream out, Object metadata2Add)
      throws IOException {

    List<IIOMetadata> metadata = new ArrayList<>();
    List<BufferedImage> images = getImages(inputImage, metadata);

    if (metadata.size() != images.size()) {
      throw new IllegalStateException();
    }

    // Obtain a TIFF writer
    ImageWriter writer = ImageIO.getImageWritersByFormatName("TIFF").next();
    try (ImageOutputStream output = ImageIO.createImageOutputStream(out)) {
      writer.setOutput(output);

      ImageWriteParam params = writer.getDefaultWriteParam();
      params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);

      // Compression: None, PackBits, ZLib, Deflate, LZW, JPEG and CCITT variants allowed
      // (different plugins may use a different set of compression type names)
      params.setCompressionType("Deflate");

      // streamMetadata is null here
      IIOMetadata streamMetadata = writer.getDefaultStreamMetadata(params);

      // TODO: add custom metadata fields [option 1]  
      writer.prepareWriteSequence(streamMetadata);
      for (int i = 0; i < images.size(); i++) {
        BufferedImage image = images.get(i);
        IIOMetadata imageMetadata = metadata.get(i);
        // TODO: add custom metadata fields [option 2]
        writer.writeToSequence(new IIOImage(image, null, imageMetadata), params);
      }
      writer.endWriteSequence();

    } finally {
      writer.dispose();
    }
  }

  private static List<BufferedImage> getImages(final InputStream inputImage,
      final List<IIOMetadata> metadata) throws IOException {
    List<BufferedImage> images = new ArrayList<>();
    ImageReader reader = null;
    try (ImageInputStream is = ImageIO.createImageInputStream(inputImage)) {
      Iterator<ImageReader> iterator = ImageIO.getImageReaders(is);
      reader = iterator.next();
      reader.setInput(is);

      int numPages = reader.getNumImages(true);
      for (int numPage = 0; numPage < numPages; numPage++) {
        BufferedImage pageImage = reader.read(numPage);
        IIOMetadata imageMetadata = reader.getImageMetadata(numPage);
        metadata.add(imageMetadata);
        images.add(pageImage);
      }
      return images;
    } finally {
      if (reader != null) {
        reader.dispose();
      }
    }
  }
}

Try to update imageMetadata [option 2] with following code does not work. What is wrong here?

IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry");
textEntry.setAttribute("keyword", "aaaaaa");
textEntry.setAttribute("value", "bbb");
            
IIOMetadataNode text = new IIOMetadataNode("tEXt");
text.appendChild(textEntry);

Node root = meta.getAsTree(formatName);
root.appendChild(text);
//e.g. formatName = "javax_imageio_1.0"
imageMetadata.setFromTree(imageMetadata.getNativeMetadataFormatName(), root);

Or is there a nicer/other way to store some further processing informations within the tiff?


Solution

  • This is my working solution.

    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.nio.charset.Charset;
    import java.nio.file.Files;
    import java.nio.file.attribute.UserDefinedFileAttributeView;
    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    import java.util.Map;
    import java.util.Map.Entry;
    import java.util.TreeMap;
    import javax.imageio.IIOImage;
    import javax.imageio.ImageIO;
    import javax.imageio.ImageReader;
    import javax.imageio.ImageWriteParam;
    import javax.imageio.ImageWriter;
    import javax.imageio.metadata.IIOMetadata;
    import javax.imageio.metadata.IIOMetadataNode;
    import javax.imageio.stream.ImageInputStream;
    import javax.imageio.stream.ImageOutputStream;
    import org.apache.commons.imaging.common.RationalNumber;
    import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoBytes;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoDouble;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoFloat;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoLong;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoRational;
    import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoShort;
    import com.twelvemonkeys.imageio.metadata.tiff.Rational;
    
    public class TiffMetadataExample {
    
      public static final int TIFF_TAG_XMP = 0x2BC;
      public static final String TIFF_TAG_XMP_NAME = "XMP";
    
      private static final String SUN_TIFF_FORMAT = "com_sun_media_imageio_plugins_tiff_image_1.0";
      private static final String SUN_TIFF_STREAM_FORMAT =
          "com_sun_media_imageio_plugins_tiff_stream_1.0";
      private static final String TAG_SET_CLASS_NAME =
          "com.sun.media.imageio.plugins.tiff.BaselineTIFFTagSet";
    
      public static void setMetaData(File in, File out, Metadata metaData) throws IOException {
        try (FileInputStream fis = new FileInputStream(in);
            FileOutputStream fos = new FileOutputStream(out)) {
          setMetaData(fis, fos, metaData);
        }
    
        UserDefinedFileAttributeView userDefView =
            Files.getFileAttributeView(out.toPath(), UserDefinedFileAttributeView.class);
    
        for (Entry<String, String> fileAttEntry : metaData.getfileAtt().entrySet()) {
          userDefView.write(fileAttEntry.getKey(),
              Charset.defaultCharset().encode(fileAttEntry.getValue()));
        }
      }
    
      public static void setMetaData(InputStream inputImage, OutputStream out, Metadata metdaData2Add)
          throws IOException {
        List<IIOMetadata> metadataList = new ArrayList<>();
        List<BufferedImage> images = getImages(inputImage, metadataList);
    
        // Obtain a TIFF writer
        ImageWriter writer = ImageIO.getImageWritersByFormatName("TIFF").next();
        try (ImageOutputStream output = ImageIO.createImageOutputStream(out)) {
          writer.setOutput(output);
    
          ImageWriteParam params = writer.getDefaultWriteParam();
          params.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
    
          // Compression: None, PackBits, ZLib, Deflate, LZW, JPEG and CCITT variants allowed
          // (different plugins may use a different set of compression type names)
          params.setCompressionType("Deflate");
    
          IIOMetadata streamMetadata = writer.getDefaultStreamMetadata(params);
          writer.prepareWriteSequence(streamMetadata);
          for (int i = 0; i < images.size(); i++) {
            BufferedImage image = images.get(i);
            IIOMetadata imageMetadata = metadataList.get(i);
            updateMetadata(imageMetadata, metdaData2Add.get());
            writer.writeToSequence(new IIOImage(image, null, imageMetadata), params);
          }
          writer.endWriteSequence();
    
        } finally {
          writer.dispose();
        }
      }
    
      private static void updateMetadata(IIOMetadata metadata, List<IIOMetadataNode> metdaData2AddList)
          throws IOException {
        if (SUN_TIFF_FORMAT.equals(metadata.getNativeMetadataFormatName())
            || SUN_TIFF_STREAM_FORMAT.equals(metadata.getNativeMetadataFormatName())) {
          // wanted format
        } else {
          throw new IllegalArgumentException(
              "Could not write tiff metadata, wrong format: " + metadata.getNativeMetadataFormatName());
        }
    
        IIOMetadataNode root = new IIOMetadataNode(metadata.getNativeMetadataFormatName());
        IIOMetadataNode ifd;
        if (root.getElementsByTagName("TIFFIFD").getLength() == 0) {
          ifd = new IIOMetadataNode("TIFFIFD");
          ifd.setAttribute("tagSets", TAG_SET_CLASS_NAME);
          root.appendChild(ifd);
        } else {
          ifd = (IIOMetadataNode) root.getElementsByTagName("TIFFIFD").item(0);
        }
    
        for (IIOMetadataNode metdaData2Add : metdaData2AddList) {
          ifd.appendChild(metdaData2Add);
        }
    
        metadata.mergeTree(metadata.getNativeMetadataFormatName(), root);
      }
    
      private static List<BufferedImage> getImages(final InputStream inputImage,
          final List<IIOMetadata> metadata) throws IOException {
        List<BufferedImage> images = new ArrayList<>();
        ImageReader reader = null;
        try (ImageInputStream is = ImageIO.createImageInputStream(inputImage)) {
          Iterator<ImageReader> iterator = ImageIO.getImageReaders(is);
          reader = iterator.next();
          reader.setInput(is);
    
          int numPages = reader.getNumImages(true);
          for (int numPage = 0; numPage < numPages; numPage++) {
            BufferedImage pageImage = reader.read(numPage);
            IIOMetadata meta = reader.getImageMetadata(numPage);
            metadata.add(meta);
            images.add(pageImage);
          }
          return images;
        } finally {
          if (reader != null) {
            reader.dispose();
          }
        }
      }
    
      public static class Metadata {
    
        private final List<IIOMetadataNode> addList = new ArrayList<>();
        private final Map<String, String> fileAtt = new TreeMap<>();
    
        public Metadata() {}
    
        private List<IIOMetadataNode> get() {
          return addList;
        }
    
        private Map<String, String> getfileAtt() {
          return fileAtt;
        }
    
        public void add(int exifTag, String exifTagName, Object val) {
          IIOMetadataNode md;
          if (val instanceof byte[]) {
            md = createBytesField(exifTag, exifTagName, (byte[]) val);
          } else if (val instanceof String) {
            md = createAsciiField(exifTag, exifTagName, (String) val);
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof Short) {
            md = createShortField(exifTag, exifTagName, ((Short) val).intValue());
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof Integer) {
            md = createShortField(exifTag, exifTagName, ((Integer) val).intValue());
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof Long) {
            md = createLongField(exifTag, exifTagName, ((Long) val).longValue());
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof Float) {
            md = createFloatField(exifTag, exifTagName, ((Float) val).floatValue());
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof Double) {
            md = createDoubleField(exifTag, exifTagName, ((Double) val).doubleValue());
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof Rational) {
            md = createRationalField(exifTag, exifTagName, ((Rational) val));
            fileAtt.put(exifTagName, String.valueOf(val));
          } else if (val instanceof RationalNumber) {
            md = createRationalField(exifTag, exifTagName, ((RationalNumber) val));
            fileAtt.put(exifTagName, String.valueOf(val));
          } else {
            throw new IllegalArgumentException("unsupported value class: " + val.getClass().getName());
          }
    
          addList.add(md);
        }
    
        /**
         * 
         * @param tagInfo {@link TiffTagConstants} like {@link TiffTagConstants#TIFF_TAG_XMP}
         * @param val String, byte[],
         */
        public void add(TagInfo tagInfo, Object val) {
          if (tagInfo instanceof TagInfoBytes) {
            if (!(val instanceof byte[])) {
              throw new IllegalArgumentException("expecting byte[] value");
            }
          } else if (tagInfo instanceof TagInfoAscii) {
            if (!(val instanceof String)) {
              throw new IllegalArgumentException("expecting String value");
            }
          } else if (tagInfo instanceof TagInfoShort) {
            if (val instanceof Short || val instanceof Integer) {
              // ok
            } else {
              throw new IllegalArgumentException("expecting Short/Integer value");
            }
          } else if (tagInfo instanceof TagInfoLong) {
            if (!(val instanceof Long)) {
              throw new IllegalArgumentException("expecting Long value");
            }
          } else if (tagInfo instanceof TagInfoDouble) {
            if (!(val instanceof Double)) {
              throw new IllegalArgumentException("expecting double value");
            }
          } else if (tagInfo instanceof TagInfoFloat) {
            if (!(val instanceof Float)) {
              throw new IllegalArgumentException("expecting float value");
            }
          } else if (tagInfo instanceof TagInfoRational) {
            if (val instanceof RationalNumber || val instanceof Rational) {
              // ok
            } else {
              throw new IllegalArgumentException("expecting rational value");
            }
          }
          add(tagInfo.tag, tagInfo.name, val);
        }
    
        private static IIOMetadataNode createBytesField(int number, String name, byte[] bytes) {
          IIOMetadataNode field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          IIOMetadataNode arrayNode = new IIOMetadataNode("TIFFBytes");
          field.appendChild(arrayNode);
    
          for (byte b : bytes) {
            IIOMetadataNode valueNode = new IIOMetadataNode("TIFFByte");
            valueNode.setAttribute("value", Integer.toString(b));
            arrayNode.appendChild(valueNode);
          }
          return field;
        }
    
        private static IIOMetadataNode createShortField(int number, String name, int val) {
          IIOMetadataNode field, arrayNode, valueNode;
          field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          arrayNode = new IIOMetadataNode("TIFFShorts");
          field.appendChild(arrayNode);
          valueNode = new IIOMetadataNode("TIFFShort");
          arrayNode.appendChild(valueNode);
          valueNode.setAttribute("value", Integer.toString(val));
          return field;
        }
    
        private static IIOMetadataNode createAsciiField(int number, String name, String val) {
          IIOMetadataNode field, arrayNode, valueNode;
          field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          arrayNode = new IIOMetadataNode("TIFFAsciis");
          field.appendChild(arrayNode);
          valueNode = new IIOMetadataNode("TIFFAscii");
          arrayNode.appendChild(valueNode);
          valueNode.setAttribute("value", val);
          return field;
        }
    
        private static IIOMetadataNode createLongField(int number, String name, long val) {
          IIOMetadataNode field, arrayNode, valueNode;
          field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          arrayNode = new IIOMetadataNode("TIFFLongs");
          field.appendChild(arrayNode);
          valueNode = new IIOMetadataNode("TIFFLong");
          arrayNode.appendChild(valueNode);
          valueNode.setAttribute("value", Long.toString(val));
          return field;
        }
    
        private static IIOMetadataNode createFloatField(int number, String name, float val) {
          IIOMetadataNode field, arrayNode, valueNode;
          field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          arrayNode = new IIOMetadataNode("TIFFFloats");
          field.appendChild(arrayNode);
          valueNode = new IIOMetadataNode("TIFFFloat");
          arrayNode.appendChild(valueNode);
          valueNode.setAttribute("value", Float.toString(val));
          return field;
        }
    
        private static IIOMetadataNode createDoubleField(int number, String name, double val) {
          IIOMetadataNode field, arrayNode, valueNode;
          field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          arrayNode = new IIOMetadataNode("TIFFDoubles");
          field.appendChild(arrayNode);
          valueNode = new IIOMetadataNode("TIFFDouble");
          arrayNode.appendChild(valueNode);
          valueNode.setAttribute("value", Double.toString(val));
          return field;
        }
    
        private static IIOMetadataNode createRationalField(int number, String name, Rational rational) {
          return createRationalField(number, name, rational.numerator(), rational.denominator());
        }
    
        private static IIOMetadataNode createRationalField(int number, String name,
            RationalNumber rational) {
          return createRationalField(number, name, rational.numerator, rational.divisor);
        }
    
        private static IIOMetadataNode createRationalField(int number, String name, long numerator,
            long denominator) {
          IIOMetadataNode field, arrayNode, valueNode;
          field = new IIOMetadataNode("TIFFField");
          field.setAttribute("number", Integer.toString(number));
          field.setAttribute("name", name);
          arrayNode = new IIOMetadataNode("TIFFRationals");
          field.appendChild(arrayNode);
          valueNode = new IIOMetadataNode("TIFFRational");
          arrayNode.appendChild(valueNode);
          valueNode.setAttribute("value", numerator + "/" + denominator);
          return field;
        }
      }
    }
    

    Usage

    byte[] bytes = create();
    
    TiffMetadata.Metadata metaData = new TiffMetadata.Metadata();
    metaData.add(TiffTagConstants.TIFF_TAG_SOFTWARE, "FUBAR");
    // metaData.add(TiffMetadata.TIFF_TAG_XMP, TiffMetadata.TIFF_TAG_XMP_NAME, bytes );
    metaData.add(TiffTagConstants.TIFF_TAG_XMP, bytes);
    
    TiffMetadata.setMetaData(tiffIn, tiffOut, metaData);