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.metadata.photoshop;
  
  import com.drew.lang.ByteArrayReader;
  import com.drew.lang.Charsets;
  import com.drew.lang.RandomAccessReader;
  import com.drew.lang.annotations.NotNull;
  import com.drew.lang.annotations.Nullable;
  import com.drew.metadata.TagDescriptor;
  
  import java.io.IOException;
  import java.text.DecimalFormat;
  import java.util.ArrayList;
  
  import static com.drew.metadata.photoshop.PhotoshopDirectory.*;
  
  /**
   * @author Drew Noakes https://drewnoakes.com
   * @author Yuri Binev
   * @author Payton Garland
   */
  @SuppressWarnings("WeakerAccess")
  public class PhotoshopDescriptor extends TagDescriptor<PhotoshopDirectory>
  {
      public PhotoshopDescriptor(@NotNull PhotoshopDirectory directory)
      {
          super(directory);
      }
  
      @Override
      public String getDescription(int tagType)
      {
          switch (tagType) {
              case TAG_THUMBNAIL:
              case TAG_THUMBNAIL_OLD:
                  return getThumbnailDescription(tagType);
              case TAG_URL:
              case TAG_XML:
                  return getSimpleString(tagType);
              case TAG_IPTC:
                  return getBinaryDataString(tagType);
              case TAG_SLICES:
                  return getSlicesDescription();
              case TAG_VERSION:
                  return getVersionDescription();
              case TAG_COPYRIGHT:
                  return getBooleanString(tagType);
              case TAG_RESOLUTION_INFO:
                  return getResolutionInfoDescription();
              case TAG_GLOBAL_ANGLE:
              case TAG_GLOBAL_ALTITUDE:
              case TAG_URL_LIST:
              case TAG_SEED_NUMBER:
                  return get32BitNumberString(tagType);
              case TAG_JPEG_QUALITY:
                  return getJpegQualityString();
              case TAG_PRINT_SCALE:
                  return getPrintScaleDescription();
              case TAG_PIXEL_ASPECT_RATIO:
                  return getPixelAspectRatioString();
              case TAG_CLIPPING_PATH_NAME:
                  return getClippingPathNameString(tagType);
              default:
                  if (tagType >= 0x07D0 && tagType <= 0x0BB6)
                      return getPathString(tagType);
                  return super.getDescription(tagType);
          }
      }
  
      @Nullable
      public String getJpegQualityString()
      {
          try {
              byte[] b = _directory.getByteArray(TAG_JPEG_QUALITY);
  
              if (b == null)
                  return _directory.getString(TAG_JPEG_QUALITY);
  
              RandomAccessReader reader = new ByteArrayReader(b);
              int q = reader.getUInt16(0); // & 0xFFFF;
              int f = reader.getUInt16(2); // & 0xFFFF;
              int s = reader.getUInt16(4);
  
              int q1 = q <= 0xFFFF && q >= 0xFFFD
                  ? q - 0xFFFC
                  : q <= 8
                      ? q + 4
                      : q;
  
              String quality;
              switch (q) {
                  case 0xFFFD:
                  case 0xFFFE:
                  case 0xFFFF:
                  case 0:
                      quality = "Low";
                      break;
                  case 1:
                  case 2:
                  case 3:
                      quality = "Medium";
                      break;
                  case 4:
                  case 5:
                      quality = "High";
                      break;
                  case 6:
                  case 7:
                  case 8:
                      quality = "Maximum";
                      break;
                  default:
                      quality = "Unknown";
              }
  
              String format;
              switch (f) {
                  case 0x0000:
                      format = "Standard";
                      break;
                  case 0x0001:
                      format = "Optimised";
                      break;
                  case 0x0101:
                      format = "Progressive";
                      break;
                  default:
                      format = String.format("Unknown 0x%04X", f);
              }
  
              String scans = s >= 1 && s <= 3
                      ? String.format("%d", s + 2)
                      : String.format("Unknown 0x%04X", s);
  
              return String.format("%d (%s), %s format, %s scans", q1, quality, format, scans);
          } catch (IOException e) {
              return null;
          }
      }
  
      @Nullable
      public String getPixelAspectRatioString()
      {
          try {
              byte[] bytes = _directory.getByteArray(TAG_PIXEL_ASPECT_RATIO);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              double d = reader.getDouble64(4);
              return Double.toString(d);
          } catch (Exception e) {
              return null;
          }
      }
  
      @Nullable
      public String getPrintScaleDescription()
      {
          try {
              byte bytes[] = _directory.getByteArray(TAG_PRINT_SCALE);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              int style = reader.getInt32(0);
              float locX = reader.getFloat32(2);
              float locY = reader.getFloat32(6);
              float scale = reader.getFloat32(10);
              switch (style) {
                  case 0:
                      return "Centered, Scale " + scale;
                  case 1:
                      return "Size to fit";
                  case 2:
                      return String.format("User defined, X:%s Y:%s, Scale:%s", locX, locY, scale);
                  default:
                      return String.format("Unknown %04X, X:%s Y:%s, Scale:%s", style, locX, locY, scale);
              }
          } catch (Exception e) {
              return null;
          }
      }
  
      @Nullable
      public String getResolutionInfoDescription()
      {
          try {
              byte[] bytes = _directory.getByteArray(TAG_RESOLUTION_INFO);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              float resX = reader.getS15Fixed16(0);
              float resY = reader.getS15Fixed16(8); // is this the correct offset? it's only reading 4 bytes each time
              DecimalFormat format = new DecimalFormat("0.##");
              return format.format(resX) + "x" + format.format(resY) + " DPI";
          } catch (Exception e) {
              return null;
          }
      }
  
      @Nullable
      public String getVersionDescription()
      {
          try {
              final byte[] bytes = _directory.getByteArray(TAG_VERSION);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              int pos = 0;
              int ver = reader.getInt32(0);
              pos += 4;
              pos++;
              int readerLength = reader.getInt32(5);
              pos += 4;
              String readerStr = reader.getString(9, readerLength * 2, "UTF-16");
              pos += readerLength * 2;
              int writerLength = reader.getInt32(pos);
              pos += 4;
              String writerStr = reader.getString(pos, writerLength * 2, "UTF-16");
              pos += writerLength * 2;
              int fileVersion = reader.getInt32(pos);
              return String.format("%d (%s, %s) %d", ver, readerStr, writerStr, fileVersion);
          } catch (IOException e) {
              return null;
          }
      }
  
      @Nullable
      public String getSlicesDescription()
      {
          try {
              final byte bytes[] = _directory.getByteArray(TAG_SLICES);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              int nameLength = reader.getInt32(20);
              String name = reader.getString(24, nameLength * 2, "UTF-16");
              int pos = 24 + nameLength * 2;
              int sliceCount = reader.getInt32(pos);
              return String.format("%s (%d,%d,%d,%d) %d Slices",
                      name, reader.getInt32(4), reader.getInt32(8), reader.getInt32(12), reader.getInt32(16), sliceCount);
          } catch (IOException e) {
              return null;
          }
      }
  
      @Nullable
      public String getThumbnailDescription(int tagType)
      {
          try {
              byte[] v = _directory.getByteArray(tagType);
              if (v == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(v);
              int format = reader.getInt32(0);
              int width = reader.getInt32(4);
              int height = reader.getInt32(8);
              //skip WidthBytes
              int totalSize = reader.getInt32(16);
              int compSize = reader.getInt32(20);
              int bpp = reader.getInt32(24);
              //skip Number of planes
              return String.format("%s, %dx%d, Decomp %d bytes, %d bpp, %d bytes",
                      format == 1 ? "JpegRGB" : "RawRGB",
                      width, height, totalSize, bpp, compSize);
          } catch (IOException e) {
              return null;
          }
      }
  
      @Nullable
      private String getBooleanString(int tag)
      {
          final byte[] bytes = _directory.getByteArray(tag);
          if (bytes == null || bytes.length == 0)
              return null;
          return bytes[0] == 0 ? "No" : "Yes";
      }
  
      @Nullable
      private String get32BitNumberString(int tag)
      {
          byte[] bytes = _directory.getByteArray(tag);
          if (bytes == null)
              return null;
          RandomAccessReader reader = new ByteArrayReader(bytes);
          try {
              return String.format("%d", reader.getInt32(0));
          } catch (IOException e) {
              return null;
          }
      }
  
      @Nullable
      private String getSimpleString(int tagType)
      {
          final byte[] bytes = _directory.getByteArray(tagType);
          if (bytes == null)
              return null;
          return new String(bytes);
      }
  
      @Nullable
      private String getBinaryDataString(int tagType)
      {
          final byte[] bytes = _directory.getByteArray(tagType);
          if (bytes == null)
              return null;
          return String.format("%d bytes binary data", bytes.length);
      }
  
      @Nullable
      public String getClippingPathNameString(int tagType)
      {
          try {
              byte[] bytes = _directory.getByteArray(tagType);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              int length = reader.getByte(0);
              return new String(reader.getBytes(1, length), "UTF-8");
          } catch (Exception e) {
              return null;
          }
      }
  
      @Nullable
      public String getPathString(int tagType)
      {
          try {
              byte[] bytes = _directory.getByteArray(tagType);
              if (bytes == null)
                  return null;
              RandomAccessReader reader = new ByteArrayReader(bytes);
              int length = (int) (reader.getLength() - reader.getByte((int)reader.getLength() - 1) - 1) / 26;
  
              String fillRecord = null;
  
              // Possible subpaths
              Subpath cSubpath = new Subpath();
              Subpath oSubpath = new Subpath();
  
              ArrayList<Subpath> paths = new ArrayList<Subpath>();
  
              // Loop through each path resource block segment (26-bytes)
              for (int i = 0; i < length; i++) {
                  // Spacer takes into account which block is currently being worked on while accessing byte array
                  int recordSpacer = 26 * i;
                  int selector = reader.getInt16(recordSpacer);
  
                  /*
                   * Subpath resource blocks come in 26-byte segments with 9 possible selectors - some selectors
                   * are formatted different from others
                   *
                   *      0 = Closed subpath length record
                   *      1 = Closed subpath Bezier knot, linked
                   *      2 = Closed subpath Bezier knot, unlinked
                   *      3 = Open subpath length record
                   *      4 = Open subpath Bezier knot, linked
                   *      5 = Open subpath Bezier knot, unlinked
                   *      6 = Subpath fill rule record
                   *      7 = Clipboard record
                   *      8 = Initial fill rule record
                   *
                   * Source: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
                   */
                  switch (selector) {
                      case 0:
                          // Insert previous Paths if there are any
                          if (cSubpath.size() != 0) {
                              paths.add(cSubpath);
                          }
  
                          // Make path size accordingly
                          cSubpath = new Subpath("Closed Subpath");
                          break;
                      case 1:
                      case 2:
                      {
                          Knot knot;
                          if (selector == 1)
                              knot = new Knot("Linked");
                          else
                              knot = new Knot("Unlinked");
                          // Insert each point into cSubpath - points are 32-bit signed, fixed point numbers and have 8-bits before the point
                          for (int j = 0; j < 6; j++) {
                              knot.setPoint(j, reader.getInt8((j * 4) + 2 + recordSpacer) + (reader.getInt24((j * 4) + 3 + recordSpacer) / Math.pow(2.0, 24.0)));
                          }
                          cSubpath.add(knot);
                          break;
                      }
                      case 3:
                          // Insert previous Paths if there are any
                          if (oSubpath.size() != 0) {
                              paths.add(oSubpath);
                          }
  
                          // Make path size accordingly
                          oSubpath = new Subpath("Open Subpath");
                          break;
                      case 4:
                      case 5:
                      {
                          Knot knot;
                          if (selector == 4)
                              knot = new Knot("Linked");
                          else
                              knot = new Knot("Unlinked");
                          // Insert each point into oSubpath - points are 32-bit signed, fixed point numbers and have 8-bits before the point
                          for (int j = 0; j < 6; j++) {
                              knot.setPoint(j, reader.getInt8((j * 4) + 2 + recordSpacer) + (reader.getInt24((j * 4) + 3 + recordSpacer) / Math.pow(2.0, 24.0)));
                          }
                          oSubpath.add(knot);
                          break;
                      }
                      case 6:
                          break;
                      case 7:
                          // TODO: Clipboard record
  //                        for (int j = 0; j < 24; j++) {
  //                           clipboardRecord[j] = bytes[j + 2 + recordSpacer];
  //                        }
                          break;
                      case 8:
                          if (reader.getInt16(2 + recordSpacer) == 1)
                              fillRecord = "with all pixels";
                          else
                              fillRecord = "without all pixels";
                          break;
                  }
              }
  
              // Add any more paths that were not added already
              if (cSubpath.size() != 0)
                  paths.add(cSubpath);
              if (oSubpath.size() != 0)
                  paths.add(oSubpath);
  
              // Extract name (previously appended to end of byte array)
              int nameLength = reader.getByte((int)reader.getLength() - 1);
              String name = reader.getString((int)reader.getLength() - nameLength - 1, nameLength, Charsets.ASCII);
  
              // Build description
              StringBuilder str = new StringBuilder();
  
              str.append('"').append(name).append('"')
                  .append(" having ");
  
              if (fillRecord != null)
                  str.append("initial fill rule \"").append(fillRecord).append("\" and ");
  
              str.append(paths.size()).append(paths.size() == 1 ? " subpath:" : " subpaths:");
  
              for (Subpath path : paths) {
                  str.append("\n- ").append(path.getType()).append(" with ").append(paths.size()).append(paths.size() == 1 ? " knot:" : " knots:");
  
                  for (Knot knot : path.getKnots()) {
                      str.append("\n  - ").append(knot.getType());
                      str.append(" (").append(knot.getPoint(0)).append(",").append(knot.getPoint(1)).append(")");
                      str.append(" (").append(knot.getPoint(2)).append(",").append(knot.getPoint(3)).append(")");
                      str.append(" (").append(knot.getPoint(4)).append(",").append(knot.getPoint(5)).append(")");
                  }
              }
  
              return str.toString();
          } catch (Exception e) {
              return null;
          }
      }
  }
  