/**
 * *********************************************************************************************************************
 *
 * @author Fabrizio Giudici
 * @version $Id$
 *     <p>********************************************************************************************************************
 */
public class PEFDecoder {
  private static final String CLASS = PEFDecoder.class.getName();
  private static final Logger logger = Logger.getLogger(CLASS);

  private int bitBuffer = 0;
  private int availableBitCount = 0;
  private boolean reset = false;
  private final int bitCountTable[] = new int[1 << 12];
  private final byte byteTable[] = new byte[1 << 12];

  public void load(final @Nonnull DataInputStream iis) throws IOException {
    final short bit0[] = new short[13];
    final short bit1[] = new short[13];

    for (int c = 0; c < 13; c++) {
      bit0[c] = iis.readShort();
    }

    for (int c = 0; c < 13; c++) {
      bit1[c] = iis.readByte();
    }

    for (int c = 0; c < 13; c++) {
      for (int i = bit0[c];
          i <= ((bit0[c] + (bitCountTable.length >> bit1[c]) - 1) & (bitCountTable.length - 1)); ) {
        bitCountTable[i] = bit1[c];
        byteTable[i] = (byte) c;
        i++;
      }
    }

    //        logger.finest(">>>> bitCountTable: %s", Arrays.toString(bitCountTable));
    //        logger.finest(">>>> byteTable:     %s", Arrays.toString(byteTable));
  }

  public int decode(
      final @Nonnegative int bitCount,
      final boolean useTable,
      final @Nonnull RAWImageInputStream iis,
      final boolean zeroAfterFF)
      throws IOException {
    if ((bitCount == 0) || (availableBitCount < 0)) {
      return 0;
    }

    int value;

    // TODO: RAWImageInputStream supports zeroAfterFF - use it and simplify this loop
    // while (!reset && (availableBitCount < bitCount) && ((value = iis.read() & 0xff) != 0xff) &&
    // // more correct, but breaks
    while (!reset
        && (availableBitCount < bitCount)
        && ((value = iis.read() & 0xff) != -1)
        && !(reset = zeroAfterFF && (value == 0xff) && (iis.read() != 0))) {
      bitBuffer = (bitBuffer << 8) | value; // (uchar)c
      availableBitCount += 8;
    }

    value = (bitBuffer << (32 - availableBitCount)) >>> (32 - bitCount);

    if (useTable) {
      availableBitCount -= bitCountTable[value];
      value = byteTable[value] & 0xff; // (uchar)...
    } else {
      availableBitCount -= bitCount;
    }

    assert (availableBitCount >= 0);

    //        logger.finest("getbithuff: returning c: %d, bitbuf: %x, vbits: %d", c, bitbuf, vbits);
    return value;
  }
}
/**
 * *********************************************************************************************************************
 *
 * @author Fabrizio Giudici
 * @version $Id$
 *     <p>********************************************************************************************************************
 */
public abstract class RAWImageReaderSpiSupport extends ImageReaderSpi {
  private static final String CLASS = RAWImageReaderSpiSupport.class.getName();
  private static final Logger logger = Logger.getLogger(CLASS);

  /** A postprocessor, if available, will be run against the loaded image. */
  private static Map<Class<?>, PostProcessor> postProcessorMapBySpiClass =
      new HashMap<Class<?>, PostProcessor>();

  /**
   * *****************************************************************************************************************
   *
   * @param names
   * @param suffixes
   * @param MIMETypes
   * @param readerClass
   * @param inputTypes
   * @param writerSpiNames
   * @param supportsStandardStreamMetadataFormat
   * @param nativeStreamMetadataFormatName
   * @param nativeStreamMetadataFormatClassName
   * @param extraStreamMetadataFormatNames
   * @param extraStreamMetadataFormatClassNames
   * @param supportsStandardImageMetadataFormat
   * @param nativeImageMetadataFormatName
   * @param nativeImageMetadataFormatClassName
   * @param extraImageMetadataFormatNames
   * @param extraImageMetadataFormatClassNames
   *     <p>****************************************************************************************************************
   */
  protected RAWImageReaderSpiSupport(
      String[] names,
      String[] suffixes,
      String[] MIMETypes,
      Class readerClass,
      Class[] inputTypes,
      String[] writerSpiNames,
      boolean supportsStandardStreamMetadataFormat,
      String nativeStreamMetadataFormatName,
      String nativeStreamMetadataFormatClassName,
      String[] extraStreamMetadataFormatNames,
      String[] extraStreamMetadataFormatClassNames,
      boolean supportsStandardImageMetadataFormat,
      String nativeImageMetadataFormatName,
      String nativeImageMetadataFormatClassName,
      String[] extraImageMetadataFormatNames,
      String[] extraImageMetadataFormatClassNames) {
    super(
        "tidalwave.it",
        Version.TAG,
        names,
        suffixes,
        MIMETypes,
        readerClass.getName(),
        inputTypes,
        writerSpiNames,
        supportsStandardStreamMetadataFormat,
        nativeStreamMetadataFormatName,
        nativeStreamMetadataFormatClassName,
        extraStreamMetadataFormatNames,
        extraStreamMetadataFormatClassNames,
        supportsStandardImageMetadataFormat,
        nativeImageMetadataFormatName,
        nativeImageMetadataFormatClassName,
        extraImageMetadataFormatNames,
        extraImageMetadataFormatClassNames);
  }

  /**
   * *****************************************************************************************************************
   *
   * @param name
   * @param suffixes
   * @param mimeType
   * @param readerClass
   *     <p>*****************************************************************************
   */
  protected RAWImageReaderSpiSupport(
      String name, String[] suffixes, String mimeType, Class readerClass) {
    this(
        new String[] {name.toLowerCase(), name.toUpperCase()},
        suffixes,
        new String[] {mimeType},
        readerClass,
        new Class[] {ImageInputStream.class}, // inputTypes
        null, // writerSpiNames
        false, // supportsStandardStreamMetadataFormat
        null, // nativeStreamMetadataFormatName
        null, // nativeStreamMetadataFormatClassName
        null, // extraStreamMetadataFormatNames
        null, // extraStreamMetadataFormatClassNames
        false, // supportsStandardImageMetadataFormat
        null, // nativeImageMetadataFormatName
        null, // nativeImageMetadataFormatClassName
        null, // extraImageMetadataFormatNames
        null); // extraImageMetadataFormatClassNam
  }

  /**
   * *****************************************************************************************************************
   *
   * @param name
   * @param suffix
   * @param mimeType
   * @param readerClass
   *     <p>*****************************************************************************
   */
  protected RAWImageReaderSpiSupport(
      String name, String suffix, String mimeType, Class readerClass) {
    this(name, new String[] {suffix.toLowerCase(), suffix.toUpperCase()}, mimeType, readerClass);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Installs a postprocessor that will be run against all the instances of images loaded by this
   * Spi.
   *
   * @param postProcessor the post processor to install
   *     <p>*****************************************************************************
   */
  public static void installPostProcessor(Class<?> spiClass, PostProcessor postProcessor) {
    postProcessorMapBySpiClass.put(spiClass, postProcessor);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Post-processes a raw image using the installed postprocessor, if any.
   *
   * @param image the raw image to postprocess
   * @return the post-processed image
   *     <p>****************************************************************************************************************
   */
  @Nonnull
  protected BufferedImage postProcess(
      final @Nonnull BufferedImage image,
      final @Nonnull RAWMetadataSupport metadata,
      final @Nonnull RAWImageReadParam readParam) {
    logger.fine("postProcess(%s, %s, %s)", image, metadata.getClass(), readParam);
    final Source source = readParam.lookup(Source.class);
    final PostProcessor postProcessor =
        !source.needsPostProcessor() ? null : postProcessorMapBySpiClass.get(getClass());
    logger.finer(">>>> source: %s, postProcessor: %s", source, postProcessor);

    return (postProcessor != null) ? postProcessor.process(image, metadata, readParam) : image;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Post-processes a raw image using the installed postprocessor, if any.
   *
   * @param image the raw image to postprocess
   * @return the post-processed image
   *     <p>****************************************************************************************************************
   */
  protected void postProcessMetadata(
      final @Nonnull RAWMetadataSupport metadata, final @Nonnull RAWImageReadParam readParam) {
    logger.fine("postProcessMetadata(%s, %s)", metadata.getClass(), readParam);
    final Source source = readParam.lookup(Source.class);
    final PostProcessor postProcessor =
        !source.needsPostProcessor() ? null : postProcessorMapBySpiClass.get(getClass());
    logger.finer(">>>> source: %s, postProcessor: %s", source, postProcessor);

    if (postProcessor != null) {
      postProcessor.processMetadata(metadata, readParam);
    }
  }

  /**
   * *****************************************************************************************************************
   *
   * @inheritDoc
   *     ****************************************************************************************************************
   */
  public boolean canDecodeInput(Object source) throws IOException {
    if (source instanceof ImageInputStream) {
      return canDecodeInput((ImageInputStream) source);
    } else {
      ImageInputStream iis = null;

      try {
        iis = ImageIO.createImageInputStream(source);

        if (iis != null) {
          return canDecodeInput(iis);
        }
      } finally {
        if (iis != null) {
          iis.close();
        }
      }
    }

    return false;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  private boolean canDecodeInput(ImageInputStream source) throws IOException {
    RAWImageInputStream iis = new RAWImageInputStreamImpl(source);
    iis
        .setDontCloseDelegate(); // otherwise the garbage collector will close it together with the
                                 // original source!!

    try {
      iis.mark();
      return canDecodeInput(iis);
    } catch (Exception e) {
      return false;
    } finally {
      iis.setBaseOffset(0);
      iis.reset();
    }
  }

  /**
   * *****************************************************************************************************************
   *
   * @param iis
   * @return
   * @throws IOException
   *     <p>****************************************************************************************************************
   */
  protected abstract boolean canDecodeInput(RAWImageInputStream iis) throws IOException;
}
/**
 * *********************************************************************************************************************
 *
 * @author Fabrizio Giudici
 * @version $Id$
 *     <p>********************************************************************************************************************
 */
public class NEFImageReaderSpi extends RAWImageReaderSpiSupport {
  private static final String CLASS = NEFImageReaderSpi.class.getName();
  private static final Logger logger = Logger.getLogger(CLASS);

  private static final List supportedModels =
      Arrays.asList(new String[] {"E5700", "E8700", "E5400", "E8800"});

  /**
   * *****************************************************************************************************************
   *
   * <p>*****************************************************************************
   */
  public NEFImageReaderSpi() {
    super("NEF", new String[] {"nef", "ndf"}, "image/x-nikon-nef", NEFImageReader.class);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnull
  public String getDescription(final Locale locale) {
    return "Standard NEF Image Reader";
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnull
  public ImageReader createReaderInstance(@CheckForNull final Object extension) throws IOException {
    return new NEFImageReader(this, extension);
  }

  /**
   * *****************************************************************************************************************
   *
   * @param iis
   * @return
   * @throws IOException
   *     <p>*****************************************************************************
   */
  protected boolean canDecodeInput(@Nonnull final RAWImageInputStream iis) throws IOException {
    iis.seek(0);
    NEFHeaderProcessor headerProcessor = new NEFHeaderProcessor();
    headerProcessor.process(iis);
    long ifdOffset = TIFFImageReaderSupport.processHeader(iis, headerProcessor);

    if (ifdOffset
        == 20) // TODO: bad fix for NDF files, must test for NDF instead - headerProcessor.isNDF()
    {
      ifdOffset -= NEFHeaderProcessor.NDF_OFFSET;
      iis.setBaseOffset(
          NEFHeaderProcessor
              .NDF_OFFSET); // TODO: move this behaviour as generic, with hproc.getBaseOffset()
    }

    IFD primaryIFD = new IFD();
    primaryIFD.load(iis, ifdOffset);

    if (primaryIFD.isDNGVersionAvailable()) {
      logger.finest(">>>> FAILING, isDNGVersionAvailable returning true");
      return false;
    }

    String make = primaryIFD.getMake();
    String model = primaryIFD.getModel();
    logger.finest("Make: %s, Model: %s", make, model);
    //
    // Beware that TIFF files out of Nikon scanners are tagged as Nikon.
    // Check the model name too.
    //
    if ((make == null)
        || !make.toUpperCase().startsWith("NIKON")
        || (model == null)
        || (!model.toUpperCase().startsWith("NIKON D")
            && !supportedModels.contains(model.toUpperCase()))) {
      logger.finest(">>>> FAILING, supportedModels: %s", supportedModels);
      return false;
    }

    return true;
  }
}
Beispiel #4
0
/**
 * *********************************************************************************************************************
 *
 * @author Fabrizio Giudici
 * @version $Id$
 *     <p>********************************************************************************************************************
 */
public abstract class Directory implements Serializable {
  private static final String CLASS = Directory.class.getName();
  private static final Logger logger = Logger.getLogger(CLASS);
  private static final long serialVersionUID = 7068468438676854749L;

  protected long start;

  protected long end;

  /** The map of getTags. */
  private final Map<Object, AbstractTag> tagMapByKey = new HashMap<Object, AbstractTag>();

  /** The list of tag keys, in the same order as they were added. */
  private final List<Object> keyList = new ArrayList<Object>();

  protected Directory nextDirectory; // FIXME it's protected - better to make private

  /** The list of sub-directories. */
  private final Collection<Directory> directoryList = new ArrayList<Directory>();

  /** The map of named directories. */
  private Map<String, Directory> directoryMapByName = new HashMap<String, Directory>();

  /** The name of the registry this tag belongs to. */
  private String registryName;

  /**
   * The registry the contained getTags belongs to. This is transient so that this class can be
   * serialized without carrying along the whole registry. Upon deserialization the link to the
   * registry must be restored by using the registryName.
   */
  protected transient TagRegistry tagRegistry;

  /**
   * *****************************************************************************************************************
   *
   * <p>This class models an enumerated value for a tag.
   *
   * <p>****************************************************************************************************************
   */
  public static class Enumeration implements Serializable {
    private static final long serialVersionUID = 4029468438676854749L;

    /** The tag value. */
    private int intValue;

    private String stringValue;

    /** The tag name. */
    private String description;

    protected Enumeration(int value, String description) {
      this.intValue = value;
      this.description = description;
    }

    protected Enumeration(int[] value, String description) {
      this(value[0], description);
    }

    protected Enumeration(String value, String description) {
      this.stringValue = value;
      this.description = description;
    }

    protected Enumeration(String[] value, String description) {
      this(value[0], description);
    }

    public int intValue() {
      return intValue;
    }

    @Override
    public String toString() {
      if (stringValue != null) {
        return description.equals("reserved") ? (description + "#" + stringValue) : description;
      } else {
        return description.equals("reserved") ? (description + "#" + intValue) : description;
      }
    }

    @Override
    public int hashCode() {
      if (stringValue != null) {
        return stringValue.hashCode();
      } else {
        return intValue;
      }
    }

    @Override
    public boolean equals(Object object) {
      if (this.getClass() != object.getClass()) {
        return false;
      }

      Enumeration e = (Enumeration) object;

      if (stringValue != null) {
        return this.stringValue.equals(e.stringValue);
      } else {
        return this.intValue == e.intValue;
      }
    }

    protected static boolean equals(int i1, int i2) {
      return i1 == i2;
    }

    protected static boolean equals(int i1[], int i2) {
      return i1[0] == i2;
    }

    protected static boolean equals(String s1, String s2) {
      if (s1 == s2) {
        return true;
      }

      if (s1 == null) {
        return false;
      }

      return trim(s1).equals(trim(s2));
    }

    private static String trim(String s) {
      return (s == null) ? null : s.trim(); // TODO: also remove leading zeros
    }
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>For de-serialization and for supporting CGLIB only. CGLIB is needed by blueMarine. For this
   * reason it must be public too.
   *
   * <p>****************************************************************************************************************
   */
  public Directory() {}

  /**
   * *****************************************************************************************************************
   *
   * <p>Creates a new <code>Directory</code> whose getTags belong to the given registry.
   *
   * @param tagRegistry the registry
   *     <p>****************************************************************************************************************
   */
  protected Directory(@Nonnull final TagRegistry tagRegistry) {
    this.tagRegistry = tagRegistry;
    registryName = tagRegistry.getName();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the registry the contained getTags belong to.
   *
   * @return the registry
   *     <p>****************************************************************************************************************
   */
  @Nonnull
  public TagRegistry getRegistry() {
    return tagRegistry;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnegative
  public long getStart() {
    return start;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnegative
  public long getEnd() {
    return end;
  }

  /**
   * *****************************************************************************************************************
   *
   * @param iis
   * @param offset
   * @throws IOException
   *     <p>****************************************************************************************************************
   */
  public abstract long load(@Nonnull final RAWImageInputStream iis, long offset) throws IOException;

  /**
   * *****************************************************************************************************************
   *
   * @param iis
   * @param offset
   * @throws IOException
   *     <p>****************************************************************************************************************
   */
  public void loadAll(@Nonnull final RAWImageInputStream iis, @Nonnull final long offset)
      throws IOException {
    load(iis, offset);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Adds a tag to this <code>Directory</code>.
   *
   * @param tag the tag to add
   *     <p>****************************************************************************************************************
   */
  public void addTag(@Nonnull final AbstractTag tag) {
    if (tag != null) {
      final int key = tag.getCode();
      tagMapByKey.put(tagRegistry.getKey(key), tag);
      keyList.add(tagRegistry.getKey(key));
    }
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Retrieves a contained tag given its key.
   *
   * @param key the tag key
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public AbstractTag getTag(@Nonnull final Object key) {
    return tagMapByKey.get(tagRegistry.getKey(key));
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Checks if this <code>Directory</code> contains a given tag.
   *
   * @param key the tag key
   * @return true if this <code>Directory</code> contains the tag
   *     <p>****************************************************************************************************************
   */
  public boolean containsTag(@Nonnull final Object key) {
    return (tagRegistry != null) && tagMapByKey.containsKey(tagRegistry.getKey(key));
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Removes a given tag from this </code>Directory</code>
   *
   * @param key the tag key
   * @return the remove tag
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public AbstractTag removeTag(@Nonnull final Object key) {
    keyList.remove(tagRegistry.getKey(key));
    return tagMapByKey.remove(tagRegistry.getKey(key));
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Removes all tags from this /code>Directory</code>.
   *
   * <p>****************************************************************************************************************
   */
  public void removeAllTags() {
    keyList.clear();
    tagMapByKey.clear();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the name of the given tag. FIXME: is meaningful here?
   *
   * @param key the tag key
   * @return the tag name
   *     <p>****************************************************************************************************************
   */
  @Nonnull
  public String getTagName(@Nonnull final Object key) {
    return tagRegistry.getTagName(((Number) key).intValue());
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>FIXME: This method is only provided for supporting it.tidalwave.image.DirectorySupport, but
   * should be replaced with a more decoupled mechanism. This method returns a plain Number when an
   * enumeration could be expected.
   *
   * <p>****************************************************************************************************************
   */
  @CheckForNull
  public Object getObject(@Nonnull final Object key) {
    final AbstractTag tag = getTag(key);
    return (tag != null) ? tag.getValue() : null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag values as bytes.
   *
   * @param key the tag key
   * @return the bytes or null if the tag is not present
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public byte[] getBytes(@Nonnull final Object key) {
    final AbstractTag tag = getTag(key);
    return (tag != null) ? tag.getByteValues() : null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag value as a byte.
   *
   * @param key the tag key
   * @return the byte
   * @throws NoSuchElementException if the tag is not present
   *     <p>****************************************************************************************************************
   */
  public int getByte(@Nonnull final Object key) {
    if (!containsTag(key)) {
      throw new NoSuchElementException("No tags with key = " + key);
    }

    return getBytes(key)[0];
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag values as rationals.
   *
   * @param key the tag key
   * @return the rationals or null if the tag is not present
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public TagRational[] getRationals(@Nonnull final Object key) {
    AbstractTag tag = getTag(key);
    return (tag != null) ? tag.getRationalValues() : null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag value as a rational.
   *
   * @param key the tag key
   * @return the rational
   * @throws NoSuchElementException if the tag is not present
   *     <p>****************************************************************************************************************
   */
  @Nonnull
  public TagRational getRational(@Nonnull final Object key) {
    if (!containsTag(key)) {
      throw new NoSuchElementException("No tags with key = " + key);
    }

    return getRationals(key)[0];
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag values as doubles.
   *
   * @param key the tag key
   * @return the doubles or null if the tag is not present
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public double[] getDoubles(@Nonnull final Object key) {
    final AbstractTag tag = getTag(key);
    return (tag != null) ? asDoubles(tag.getRationalValues()) : null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag value as a double.
   *
   * @param key the tag key
   * @return the double
   * @throws NoSuchElementException if the tag is not present
   *     <p>****************************************************************************************************************
   */
  public double getDouble(@Nonnull final Object key) {
    if (!containsTag(key)) {
      throw new NoSuchElementException("No tags with key = " + key);
    }

    return getRationals(key)[0].doubleValue();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag values as floats.
   *
   * @param key the tag key
   * @return the floats or null if the tag is not present
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public float[] getFloats(@Nonnull final Object key) {
    final AbstractTag tag = getTag(key);
    return (tag != null) ? tag.getFloatValues() : null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag value as a float.
   *
   * @param key the tag key
   * @return the float
   * @throws NoSuchElementException if the tag is not present
   *     <p>****************************************************************************************************************
   */
  public float getFloat(@Nonnull final Object key) {
    if (!containsTag(key)) {
      throw new NoSuchElementException("No tags with key = " + key);
    }

    return getRationals(key)[0].floatValue();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag values as integers.
   *
   * @param key the tag key
   * @return the integers or null if the tag is not present
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public int[] getIntegers(@Nonnull final Object key) {
    final AbstractTag tag = getTag(key);

    if (tag == null) {
      return null;
    }

    int[] intValues = tag.getIntValues();

    if (intValues != null) {
      return intValues;
    }

    final byte[] byteValues = tag.getByteValues();

    if (byteValues != null) {
      intValues = new int[byteValues.length];

      for (int i = 0; i < byteValues.length; i++) {
        intValues[i] = byteValues[i] & 0xff;
      }

      return intValues;
    }

    return null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag value as an integer. <br>
   * This method returns 0 if there exists a tag with the given key which contains an empty array of
   * integers. Such a tag is BayerGreenSplit in DNG files. It should be clarified if this is
   * compliant with specs (in which case this behaviour should be extended to other multiple-value
   * getXXX() methods, or if it is a bug of Adobe Converter.
   *
   * @param key the tag key
   * @return the integer
   * @throws NoSuchElementException if the tag is not present
   *     <p>****************************************************************************************************************
   */
  public int getInteger(@Nonnull final Object key) throws NoSuchElementException {
    if (!containsTag(key)) {
      throw new NoSuchElementException("No tags with key = " + key);
    }

    int[] i = getIntegers(key);

    if (i.length == 0) // FIXME: happens with a BayerGreenSplit field in DNG files
    {
      return 0;
    }

    return i[0];
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the tag value as a string.
   *
   * @param key the tag key
   * @return the string or null if the tag has not been found
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public String getString(@Nonnull final Object key) {
    final AbstractTag tag = getTag(key);
    return (tag != null) ? tag.getASCIIValue() : null;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @CheckForNull
  public Directory getNextDirectory() {
    return nextDirectory;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Adds a sub-directory.
   *
   * @param subDirectory the sub-directory
   *     <p>****************************************************************************************************************
   */
  public void addDirectory(@Nonnull final Directory subDirectory) {
    directoryList.add(subDirectory);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns an iterator over sub-directories. Note that named directories are not returned.
   *
   * @return the yterator
   *     <p>****************************************************************************************************************
   */
  @Nonnull
  public Collection<Directory> getSubDirectories() {
    return Collections.unmodifiableCollection(directoryList);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Adds a named sub-directory.
   *
   * @param name the directory name
   * @param subDirectory the sub-directory
   *     <p>****************************************************************************************************************
   */
  public void addNamedDirectory(@Nonnull final String name, @Nonnull final Directory subDirectory) {
    directoryMapByName.put(name, subDirectory);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns a named sub-directory
   *
   * @param name the sub-directory name
   * @return the sub-directory
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public Directory getNamedDirectory(@Nonnull final String name) {
    return directoryMapByName.get(name);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the names of named sub directories
   *
   * @return the subdirectory names
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public String[] getSubDirectoryNames() {
    return directoryMapByName.keySet().toArray(new String[0]);
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Returns the contained getTags in the same order as they were added.
   *
   * @return the contained getTags
   *     <p>****************************************************************************************************************
   */
  @Nonnull
  public Collection<AbstractTag> getTags() {
    final List<AbstractTag> result = new ArrayList<AbstractTag>();

    for (final Object key : keyList) {
      result.add(tagMapByKey.get(key));
    }

    return result;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Convenience method that converts an array of rationals into floats.
   *
   * @param rationals
   * @return the floats
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public static float[] asFloats(@CheckForNull final TagRational[] rationals) {
    if (rationals == null) {
      return null;
    }

    float[] floats = new float[rationals.length];

    for (int i = 0; i < rationals.length; i++) {
      floats[i] = rationals[i].floatValue();
    }

    return floats;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Convenience method that converts an array of rationals into doubles.
   *
   * @param rationals
   * @return the doubles
   *     <p>****************************************************************************************************************
   */
  @CheckForNull
  public static double[] asDoubles(@CheckForNull final TagRational[] rationals) {
    if (rationals == null) {
      return null;
    }

    double[] doubles = new double[rationals.length];

    for (int i = 0; i < rationals.length; i++) {
      doubles[i] = rationals[i].doubleValue();
    }

    return doubles;
  }

  /**
   * *****************************************************************************************************************
   *
   * @inheritDoc
   *     ****************************************************************************************************************
   */
  @Override
  @Nonnull
  public String toString() {
    final StringBuilder buffer = new StringBuilder();
    buffer.append(String.format("%d - %d (0x%x - 0x%x)", start, end, start, end));

    for (final Object key : keyList) {
      buffer.append("\n>>>>>>>> ");
      final Object value = tagMapByKey.get(key);
      buffer.append(value);
    }

    buffer.append("\n");
    return buffer.toString();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnull
  protected String toString(@Nonnull final byte[] array) {
    if (array.length > 64) {
      return "" + array.length + " bytes";
    }

    final StringBuilder buffer = new StringBuilder("");

    for (int i = 0; i < array.length; i++) {
      if (i > 0) {
        buffer.append(",");
      }

      buffer.append(Integer.toHexString(array[i] & 0xff));
    }

    return buffer.toString();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnull
  protected String toString(@Nonnull final int[] array) {
    if (array.length > 64) {
      return "" + array.length + " integers";
    }

    final StringBuilder buffer = new StringBuilder("");

    for (int i = 0; i < array.length; i++) {
      if (i > 0) {
        buffer.append(",");
      }

      buffer.append(Integer.toString(array[i]));
    }

    return buffer.toString();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnull
  protected String toString(@Nonnull final double[] array) {
    if (array.length > 64) {
      return "" + array.length + " doubles";
    }

    final StringBuilder buffer = new StringBuilder("");

    for (int i = 0; i < array.length; i++) {
      if (i > 0) {
        buffer.append(",");
      }

      buffer.append(Double.toString(array[i]));
    }

    return buffer.toString();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>****************************************************************************************************************
   */
  @Nonnull
  protected String toString(@Nonnull final TagRational[] array) {
    final StringBuilder buffer = new StringBuilder("");

    for (int i = 0; i < array.length; i++) {
      if (i > 0) {
        buffer.append(",");
      }

      buffer.append(array[i].toString());
    }

    return buffer.toString();
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>Customized deserialization code. This method restores the link to the registry this tag
   * belongs to.
   *
   * @param is
   * @throws IOException
   * @throws ClassNotFoundException
   *     <p>****************************************************************************************************************
   */
  private void readObject(@Nonnull final ObjectInputStream is)
      throws IOException, ClassNotFoundException {
    is.defaultReadObject();
    tagRegistry = TagRegistry.getRegistry(registryName);
  }
}
/**
 * *********************************************************************************************************************
 *
 * @author fritz
 * @version $Id$
 *     <p>********************************************************************************************************************
 */
public class CRWImageInputStream extends RAWImageInputStreamImpl {
  private static final String CLASS = CRWImageInputStream.class.getName();
  private static final Logger logger = Logger.getLogger(CLASS);

  private static final String[] THM_EXTENSIONS = {"THM", "Thm", "thm"};

  private ImageInputStream crwInputStream;

  private ImageInputStream thmInputStream;

  /**
   * *****************************************************************************************************************
   *
   * @param delegate
   * @throws IOException
   *     <p>*****************************************************************************
   */
  public CRWImageInputStream(ImageInputStream delegate) throws IOException {
    super(delegate);
    crwInputStream = delegate;

    if (delegate instanceof FileImageInputStream2) {
      File file = ((FileImageInputStream2) delegate).getFile();
      File thmFile = null;
      String path = file.getAbsolutePath();

      int i = path.lastIndexOf('.');

      if (i > 0) {
        path = path.substring(0, i + 1);
      }

      for (i = 0; i < THM_EXTENSIONS.length; i++) {
        String thmPath = path + THM_EXTENSIONS[i];
        thmFile = new File(thmPath);

        if (thmFile.exists()) {
          break;
        }
      }

      if (!thmFile.exists()) {
        logger.warning("File " + thmFile + " does not exist");
      } else {
        logger.fine("THM file is %s", thmFile);
        thmInputStream = new FileImageInputStream(thmFile);
      }
    } else {
      logger.warning("delegate is " + delegate + ", won't be able to manage .THM file");
    }
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>*****************************************************************************
   */
  public void switchToCRWStream() {
    super.delegate = crwInputStream;
  }

  /**
   * *****************************************************************************************************************
   *
   * <p>*****************************************************************************
   */
  public boolean switchToTHMStream() {
    if (thmInputStream != null) {
      super.delegate = thmInputStream;
      return true;
    }

    return false;
  }

  /**
   * *****************************************************************************************************************
   *
   * @inheritDoc *****************************************************************************
   */
  @Override
  public void close() throws IOException {
    try {
      super.close();
    } finally {
      if (thmInputStream != null) {
        thmInputStream.close(); // TODO: should catch exceptions here?
      }
    }
  }
}