content: /*
   * Copyright 2002-2019 Drew Noakes and contributors
   *
   *    Licensed under the Apache License, Version 2.0 (the "License");
   *    you may not use this file except in compliance with the License.
   *    You may obtain a copy of the License at
   *
   *        https://www.apache.org/licenses/LICENSE-2.0
   *
   *    Unless required by applicable law or agreed to in writing, software
   *    distributed under the License is distributed on an "AS IS" BASIS,
   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   *    See the License for the specific language governing permissions and
   *    limitations under the License.
   *
   * More information about this project is available at:
   *
   *    https://drewnoakes.com/code/exif/
   *    https://github.com/drewnoakes/metadata-extractor
   */
  package com.drew.imaging.png;
  
  import com.drew.lang.annotations.NotNull;
  
  import java.io.UnsupportedEncodingException;
  import java.util.Arrays;
  import java.util.HashSet;
  import java.util.Set;
  
  /**
   * @author Drew Noakes https://drewnoakes.com
   */
  public class PngChunkType
  {
      private static final Set<String> _identifiersAllowingMultiples
          = new HashSet<String>(Arrays.asList("IDAT", "sPLT", "iTXt", "tEXt", "zTXt"));
  
      //
      // Standard critical chunks
      //
      /**
       * Denotes a critical {@link PngChunk} that contains basic information about the PNG image.
       * This must be the first chunk in the data sequence, and may only occur once.
       * <p>
       * The format is:
       * <ul>
       *     <li><b>pixel width</b> 4 bytes, unsigned and greater than zero</li>
       *     <li><b>pixel height</b> 4 bytes, unsigned and greater than zero</li>
       *     <li><b>bit depth</b> 1 byte, number of bits per sample or per palette index (not per pixel)</li>
       *     <li><b>color type</b> 1 byte, maps to {@link PngColorType} enum</li>
       *     <li><b>compression method</b> 1 byte, currently only a value of zero (deflate/inflate) is in the standard</li>
       *     <li><b>filter method</b> 1 byte, currently only a value of zero (adaptive filtering with five basic filter types) is in the standard</li>
       *     <li><b>interlace method</b> 1 byte, indicates the transmission order of image data, currently only 0 (no interlace) and 1 (Adam7 interlace) are in the standard</li>
       * </ul>
       */
      public static final PngChunkType IHDR;
  
      /**
       * Denotes a critical {@link PngChunk} that contains palette entries.
       * This chunk should only appear for a {@link PngColorType} of <code>IndexedColor</code>,
       * and may only occur once in the PNG data sequence.
       * <p>
       * The chunk contains between one and 256 entries, each of three bytes:
       * <ul>
       *     <li><b>red</b> 1 byte</li>
       *     <li><b>green</b> 1 byte</li>
       *     <li><b>blue</b> 1 byte</li>
       * </ul>
       * The number of entries is determined by the chunk length. A chunk length indivisible by three is an error.
       */
      public static final PngChunkType PLTE;
      public static final PngChunkType IDAT;
      public static final PngChunkType IEND;
  
      //
      // Standard ancillary chunks
      //
      public static final PngChunkType cHRM;
      public static final PngChunkType gAMA;
      public static final PngChunkType iCCP;
      public static final PngChunkType sBIT;
      public static final PngChunkType sRGB;
      public static final PngChunkType bKGD;
      public static final PngChunkType hIST;
      public static final PngChunkType tRNS;
      public static final PngChunkType pHYs;
      public static final PngChunkType sPLT;
      public static final PngChunkType tIME;
      public static final PngChunkType iTXt;
      public static final PngChunkType eXIf;
  
      /**
       * Denotes an ancillary {@link PngChunk} that contains textual data, having first a keyword and then a value.
       * If multiple text data keywords are needed, then multiple chunks are included in the PNG data stream.
       * <p>
       * The format is:
       * <ul>
       *     <li><b>keyword</b> 1-79 bytes</li>
       *     <li><b>null separator</b> 1 byte (\0)</li>
       *     <li><b>text string</b> 0 or more bytes</li>
       * </ul>
       * Text is interpreted according to the Latin-1 character set [ISO-8859-1].
       * Newlines should be represented by a single linefeed character (0x9).
       */
      public static final PngChunkType tEXt;
      public static final PngChunkType zTXt;
  
      static {
          try {
              IHDR = new PngChunkType("IHDR");
              PLTE = new PngChunkType("PLTE");
              IDAT = new PngChunkType("IDAT", true);
              IEND = new PngChunkType("IEND");
              cHRM = new PngChunkType("cHRM");
              gAMA = new PngChunkType("gAMA");
              iCCP = new PngChunkType("iCCP");
              sBIT = new PngChunkType("sBIT");
              sRGB = new PngChunkType("sRGB");
              bKGD = new PngChunkType("bKGD");
              hIST = new PngChunkType("hIST");
              tRNS = new PngChunkType("tRNS");
              pHYs = new PngChunkType("pHYs");
              sPLT = new PngChunkType("sPLT", true);
              tIME = new PngChunkType("tIME");
              iTXt = new PngChunkType("iTXt", true);
              tEXt = new PngChunkType("tEXt", true);
              zTXt = new PngChunkType("zTXt", true);
              eXIf = new PngChunkType("eXIf");
          } catch (PngProcessingException e) {
              throw new IllegalArgumentException(e);
          }
      }
  
      private final byte[] _bytes;
      private final boolean _multipleAllowed;
  
      public PngChunkType(@NotNull String identifier) throws PngProcessingException
      {
          this(identifier, false);
      }
  
      public PngChunkType(@NotNull String identifier, boolean multipleAllowed) throws PngProcessingException
      {
          _multipleAllowed = multipleAllowed;
  
          try {
              byte[] bytes = identifier.getBytes("ASCII");
              validateBytes(bytes);
              _bytes = bytes;
          } catch (UnsupportedEncodingException e) {
              throw new IllegalArgumentException("Unable to convert string code to bytes.");
          }
      }
  
      public PngChunkType(@NotNull byte[] bytes) throws PngProcessingException
      {
          validateBytes(bytes);
          _bytes = bytes;
          _multipleAllowed = _identifiersAllowingMultiples.contains(getIdentifier());
      }
  
      private static void validateBytes(byte[] bytes) throws PngProcessingException
      {
          if (bytes.length != 4) {
              throw new PngProcessingException("PNG chunk type identifier must be four bytes in length");
          }
  
          for (byte b : bytes) {
              if (!isValidByte(b)) {
                  throw new PngProcessingException("PNG chunk type identifier may only contain alphabet characters");
              }
          }
      }
  
      public boolean isCritical()
      {
          return isUpperCase(_bytes[0]);
      }
  
      public boolean isAncillary()
      {
          return !isCritical();
      }
  
      public boolean isPrivate()
      {
          return isUpperCase(_bytes[1]);
      }
  
      public boolean isSafeToCopy()
      {
          return isLowerCase(_bytes[3]);
      }
  
      public boolean areMultipleAllowed()
      {
          return _multipleAllowed;
      }
  
      private static boolean isLowerCase(byte b)
      {
          return (b & (1 << 5)) != 0;
      }
  
      private static boolean isUpperCase(byte b)
      {
          return (b & (1 << 5)) == 0;
      }
  
      private static boolean isValidByte(byte b)
      {
          return (b >= 65 && b <= 90) || (b >= 97 && b <= 122);
      }
  
      public String getIdentifier()
      {
          try {
              return new String(_bytes, "ASCII");
          } catch (UnsupportedEncodingException e) {
              // The constructor should ensure that we're always able to encode the bytes in ASCII.
              // noinspection ConstantConditions
              assert(false);
              return "Invalid object instance";
          }
      }
  
      @Override
      public String toString()
      {
          return getIdentifier();
      }
  
      @Override
      public boolean equals(Object o)
      {
          if (this == o)
              return true;
  
          if (o == null || getClass() != o.getClass())
              return false;
  
          PngChunkType that = (PngChunkType)o;
  
          return Arrays.equals(_bytes, that._bytes);
      }
  
      @Override
      public int hashCode()
      {
          return Arrays.hashCode(_bytes);
      }
  }
  