docs: @Nullable
      public String getFlashControlModeDescription()
      {
          int[] values = _directory.getIntArray(TagFlashControlMode);
          if (values == null)
              return null;
  
          if (values.length == 0)
              return null;
  
          StringBuilder sb = new StringBuilder();
  
          switch (values[0]) {
              case 0:
                  sb.append("Off");
                  break;
              case 3:
                  sb.append("TTL");
                  break;
              case 4:
                  sb.append("Auto");
                  break;
              case 5:
                  sb.append("Manual");
                  break;
              default:
                  sb.append("Unknown (").append(values[0]).append(")");
                  break;
          }
  
          for (int i = 1; i < values.length; i++)
              sb.append("; ").append(values[i]);
  
          return sb.toString();
      }
  
      /// <summary>
      /// 3 or 4 values
      /// </summary>
      /// <returns></returns>
      @Nullable
      public String getFlashIntensityDescription()
      {
          Rational[] values = _directory.getRationalArray(TagFlashIntensity);
          if (values == null || values.length == 0)
              return null;
  
          if (values.length == 3) {
              if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0)
                  return "n/a";
          } else if (values.length == 4) {
              if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0)
                  return "n/a (x4)";
          }
  
          StringBuilder sb = new StringBuilder();
          for (Rational t : values)
              sb.append(t).append(", ");
  
          return sb.substring(0, sb.length() - 2);
      }
  
      @Nullable
      public String getManualFlashStrengthDescription()
      {
          Rational[] values = _directory.getRationalArray(TagManualFlashStrength);
          if (values == null || values.length == 0)
              return "n/a";
  
          if (values.length == 3) {
              if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0)
                  return "n/a";
          } else if (values.length == 4) {
              if (values[0].getDenominator() == 0 && values[1].getDenominator() == 0 && values[2].getDenominator() == 0 && values[3].getDenominator() == 0)
                  return "n/a (x4)";
          }
  
          StringBuilder sb = new StringBuilder();
          for (Rational t : values)
              sb.append(t).append(", ");
  
          return sb.substring(0, sb.length() - 2);
      }
  
      @Nullable
      public String getWhiteBalance2Description()
      {
          Integer value = _directory.getInteger(TagWhiteBalance2);
          if (value == null)
              return null;
  
          switch (value) {
              case 0:
                  return "Auto";
              case 1:
                  return "Auto (Keep Warm Color Off)";
              case 16:
                  return "7500K (Fine Weather with Shade)";
              case 17:
                  return "6000K (Cloudy)";
              case 18:
                  return "5300K (Fine Weather)";
              case 20:
                  return "3000K (Tungsten light)";
              case 21:
                  return "3600K (Tungsten light-like)";
              case 22:
                  return "Auto Setup";
              case 23:
                  return "5500K (Flash)";
              case 33:
                  return "6600K (Daylight fluorescent)";
              case 34:
                  return "4500K (Neutral white fluorescent)";
              case 35:
                  return "4000K (Cool white fluorescent)";
              case 36:
                  return "White Fluorescent";
              case 48:
                  return "3600K (Tungsten light-like)";
              case 67:
                  return "Underwater";
              case 256:
                  return "One Touch WB 1";
              case 257:
                  return "One Touch WB 2";
              case 258:
                  return "One Touch WB 3";
              case 259:
                  return "One Touch WB 4";
              case 512:
                  return "Custom WB 1";
              case 513:
                  return "Custom WB 2";
              case 514:
                  return "Custom WB 3";
              case 515:
                  return "Custom WB 4";
              default:
                  return "Unknown (" + value + ")";
          }
      }
  
      @Nullable
      public String getWhiteBalanceTemperatureDescription()
      {
          Integer value = _directory.getInteger(TagWhiteBalanceTemperature);
          if (value == null)
              return null;
          if (value == 0)
              return "Auto";
          return value.toString();
      }
  
      @Nullable
      public String getCustomSaturationDescription()
      {
          // TODO: if model is /^E-1\b/  then
          // $a-=$b; $c-=$b;
          // return "CS$a (min CS0, max CS$c)"
          return getValueMinMaxDescription(TagCustomSaturation);
      }
  
      @Nullable
      public String getModifiedSaturationDescription()
      {
          return getIndexedDescription(TagModifiedSaturation,
              "Off", "CM1 (Red Enhance)", "CM2 (Green Enhance)", "CM3 (Blue Enhance)", "CM4 (Skin Tones)");
      }
  
      @Nullable
      public String getContrastSettingDescription()
      {
          return getValueMinMaxDescription(TagContrastSetting);
      }
  
      @Nullable
      public String getSharpnessSettingDescription()
      {
          return getValueMinMaxDescription(TagSharpnessSetting);
      }
  
      @Nullable
      public String getColorSpaceDescription()
      {
          return getIndexedDescription(TagColorSpace,
              "sRGB", "Adobe RGB", "Pro Photo RGB");
      }
  
      @Nullable
      public String getSceneModeDescription()
      {
          Integer value = _directory.getInteger(TagSceneMode);
          if (value == null)
              return null;
  
          switch (value) {
              case 0:
                  return "Standard";
              case 6:
                  return "Auto";
              case 7:
                  return "Sport";
              case 8:
                  return "Portrait";
              case 9:
                  return "Landscape+Portrait";
              case 10:
                  return "Landscape";
              case 11:
                  return "Night Scene";
              case 12:
                  return "Self Portrait";
              case 13:
                  return "Panorama";
              case 14:
                  return "2 in 1";
              case 15:
                  return "Movie";
              case 16:
                  return "Landscape+Portrait";
              case 17:
                  return "Night+Portrait";
              case 18:
                  return "Indoor";
              case 19:
                  return "Fireworks";
              case 20:
                  return "Sunset";
              case 21:
                  return "Beauty Skin";
              case 22:
                  return "Macro";
              case 23:
                  return "Super Macro";
              case 24:
                  return "Food";
              case 25:
                  return "Documents";
              case 26:
                  return "Museum";
              case 27:
                  return "Shoot & Select";
              case 28:
                  return "Beach & Snow";
              case 29:
                  return "Self Portrait+Timer";
              case 30:
                  return "Candle";
              case 31:
                  return "Available Light";
              case 32:
                  return "Behind Glass";
              case 33:
                  return "My Mode";
              case 34:
                  return "Pet";
              case 35:
                  return "Underwater Wide1";
              case 36:
                  return "Underwater Macro";
              case 37:
                  return "Shoot & Select1";
              case 38:
                  return "Shoot & Select2";
              case 39:
                  return "High Key";
              case 40:
                  return "Digital Image Stabilization";
              case 41:
                  return "Auction";
              case 42:
                  return "Beach";
              case 43:
                  return "Snow";
              case 44:
                  return "Underwater Wide2";
              case 45:
                  return "Low Key";
              case 46:
                  return "Children";
              case 47:
                  return "Vivid";
              case 48:
                  return "Nature Macro";
              case 49:
                  return "Underwater Snapshot";
              case 50:
                  return "Shooting Guide";
              case 54:
                  return "Face Portrait";
              case 57:
                  return "Bulb";
              case 59:
                  return "Smile Shot";
              case 60:
                  return "Quick Shutter";
              case 63:
                  return "Slow Shutter";
              case 64:
                  return "Bird Watching";
              case 65:
                  return "Multiple Exposure";
              case 66:
                  return "e-Portrait";
              case 67:
                  return "Soft Background Shot";
              case 142:
                  return "Hand-held Starlight";
              case 154:
                  return "HDR";
              default:
                  return "Unknown (" + value + ")";
          }
      }
  
      @Nullable
      public String getNoiseReductionDescription()
      {
          Integer value = _directory.getInteger(TagNoiseReduction);
          if (value == null)
              return null;
  
          if (value == 0)
              return "(none)";
  
          StringBuilder sb = new StringBuilder();
          int v = value;
  
          if ((v & 1) != 0) sb.append("Noise Reduction, ");
          if (((v >> 1) & 1) != 0) sb.append("Noise Filter, ");
          if (((v >> 2) & 1) != 0) sb.append("Noise Filter (ISO Boost), ");
          if (((v >> 3) & 1) != 0) sb.append("Auto, ");
  
          return sb.length() != 0
              ? sb.substring(0, sb.length() - 2)
              : "(none)";
      }
  
      @Nullable
      public String getDistortionCorrectionDescription()
      {
          return getIndexedDescription(TagDistortionCorrection, "Off", "On");
      }
  
      @Nullable
      public String getShadingCompensationDescription()
      {
          return getIndexedDescription(TagShadingCompensation, "Off", "On");
      }
  
      /// <summary>
      /// 3 or 4 values
      /// </summary>
      /// <returns></returns>
      @Nullable
      public String getGradationDescription()
      {
          int[] values = _directory.getIntArray(TagGradation);
          if (values == null || values.length < 3)
              return null;
  
          String join = String.format("%d %d %d", values[0], values[1], values[2]);
  
          String ret;
          if (join.equals("0 0 0")) {
              ret = "n/a";
          } else if (join.equals("-1 -1 1")) {
              ret = "Low Key";
          } else if (join.equals("0 -1 1")) {
              ret = "Normal";
          } else if (join.equals("1 -1 1")) {
              ret = "High Key";
          } else {
              ret = "Unknown (" + join + ")";
          }
  
          if (values.length > 3) {
              if (values[3] == 0)
                  ret += "; User-Selected";
              else if (values[3] == 1)
                  ret += "; Auto-Override";
          }
  
          return ret;
      }
  
      /// <summary>
      /// 1 or 2 values
      /// </summary>
      /// <returns></returns>
      @Nullable
      public String getPictureModeDescription()
      {
          int[] values = _directory.getIntArray(TagPictureMode);
          if (values == null) {
              // check if it's only one value long also
              Integer value = _directory.getInteger(TagNoiseReduction);
              if (value == null)
                  return null;
  
              values = new int[]{value};
          }
  
          if (values.length == 0)
              return null;
  
          StringBuilder sb = new StringBuilder();
          switch (values[0]) {
              case 1:
                  sb.append("Vivid");
                  break;
              case 2:
                  sb.append("Natural");
                  break;
              case 3:
                  sb.append("Muted");
                  break;
              case 4:
                  sb.append("Portrait");
                  break;
              case 5:
                  sb.append("i-Enhance");
                  break;
              case 256:
                  sb.append("Monotone");
                  break;
              case 512:
                  sb.append("Sepia");
                  break;
              default:
                  sb.append("Unknown (").append(values[0]).append(")");
                  break;
          }
  
          if (values.length > 1)
              sb.append("; ").append(values[1]);
  
          return sb.toString();
      }
  
      @Nullable
      public String getPictureModeSaturationDescription()
      {
          return getValueMinMaxDescription(TagPictureModeSaturation);
      }
  
      @Nullable
      public String getPictureModeContrastDescription()
      {
          return getValueMinMaxDescription(TagPictureModeContrast);
      }
  
      @Nullable
      public String getPictureModeSharpnessDescription()
      {
          return getValueMinMaxDescription(TagPictureModeSharpness);
      }
  
      @Nullable
      public String getPictureModeBWFilterDescription()
      {
          return getIndexedDescription(TagPictureModeBWFilter,
              "n/a", "Neutral", "Yellow", "Orange", "Red", "Green");
      }
  
      @Nullable
      public String getPictureModeToneDescription()
      {
          return getIndexedDescription(TagPictureModeTone,
              "n/a", "Neutral", "Sepia", "Blue", "Purple", "Green");
      }
  
      @Nullable
      public String getNoiseFilterDescription()
      {
          int[] values = _directory.getIntArray(TagNoiseFilter);
          if (values == null)
              return null;
  
          String join = String.format("%d %d %d", values[0], values[1], values[2]);
  
          if (join.equals("0 0 0"))
              return "n/a";
          if (join.equals("-2 -2 1"))
              return "Off";
          if (join.equals("-1 -2 1"))
              return "Low";
          if (join.equals("0 -2 1"))
              return "Standard";
          if (join.equals("1 -2 1"))
              return "High";
          return "Unknown (" + join + ")";
      }
  
      @Nullable
      public String getArtFilterDescription()
      {
          return getFiltersDescription(TagArtFilter);
      }
  
      @Nullable
      public String getMagicFilterDescription()
      {
          return getFiltersDescription(TagMagicFilter);
      }
  
      @Nullable
      public String getPictureModeEffectDescription()
      {
          int[] values = _directory.getIntArray(TagPictureModeEffect);
          if (values == null)
              return null;
  
          String key = String.format("%d %d %d", values[0], values[1], values[2]);
          if (key.equals("0 0 0"))
              return "n/a";
          if (key.equals("-1 -1 1"))
              return "Low";
          if (key.equals("0 -1 1"))
              return "Standard";
          if (key.equals("1 -1 1"))
              return "High";
          return "Unknown (" + key + ")";
      }
  
      @Nullable
      public String getToneLevelDescription()
      {
          int[] values = _directory.getIntArray(TagToneLevel);
          if (values == null || values.length == 0)
              return null;
  
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < values.length; i++) {
              if (i == 0 || i == 4 || i == 8 || i == 12 || i == 16 || i == 20 || i == 24)
                  sb.append(_toneLevelType.get(values[i])).append("; ");
              else
                  sb.append(values[i]).append("; ");
          }
  
          return sb.substring(0, sb.length() - 2);
      }
  
      @Nullable
      public String getArtFilterEffectDescription()
      {
          int[] values = _directory.getIntArray(TagArtFilterEffect);
          if (values == null)
              return null;
  
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < values.length; i++) {
              if (i == 0) {
                  sb.append((_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]")).append("; ");
              } else if (i == 3) {
                  sb.append("Partial Color ").append(values[i]).append("; ");
              } else if (i == 4) {
                  switch (values[i]) {
                      case 0x0000:
                          sb.append("No Effect");
                          break;
                      case 0x8010:
                          sb.append("Star Light");
                          break;
                      case 0x8020:
                          sb.append("Pin Hole");
                          break;
                      case 0x8030:
                          sb.append("Frame");
                          break;
                      case 0x8040:
                          sb.append("Soft Focus");
                          break;
                      case 0x8050:
                          sb.append("White Edge");
                          break;
                      case 0x8060:
                          sb.append("B&W");
                          break;
                      default:
                          sb.append("Unknown (").append(values[i]).append(")");
                          break;
                  }
                  sb.append("; ");
              } else if (i == 6) {
                  switch (values[i]) {
                      case 0:
                          sb.append("No Color Filter");
                          break;
                      case 1:
                          sb.append("Yellow Color Filter");
                          break;
                      case 2:
                          sb.append("Orange Color Filter");
                          break;
                      case 3:
                          sb.append("Red Color Filter");
                          break;
                      case 4:
                          sb.append("Green Color Filter");
                          break;
                      default:
                          sb.append("Unknown (").append(values[i]).append(")");
                          break;
                  }
                  sb.append("; ");
              } else {
                  sb.append(values[i]).append("; ");
              }
          }
  
          return sb.substring(0, sb.length() - 2);
      }
  
      @Nullable
      public String getColorCreatorEffectDescription()
      {
          int[] values = _directory.getIntArray(TagColorCreatorEffect);
          if (values == null)
              return null;
  
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < values.length; i++) {
              if (i == 0) {
                  sb.append("Color ").append(values[i]).append("; ");
              } else if (i == 3) {
                  sb.append("Strength ").append(values[i]).append("; ");
              } else {
                  sb.append(values[i]).append("; ");
              }
          }
  
          return sb.substring(0, sb.length() - 2);
      }
  
      /// <summary>
      /// 2 or 3 numbers: 1. Mode, 2. Shot number, 3. Mode bits
      /// </summary>
      /// <returns></returns>
      @Nullable
      public String getDriveModeDescription()
      {
          int[] values = _directory.getIntArray(TagDriveMode);
          if (values == null)
              return null;
  
          if (values.length == 0 || values[0] == 0)
              return "Single Shot";
  
          StringBuilder a = new StringBuilder();
  
          if (values[0] == 5 && values.length >= 3) {
              int c = values[2];
              if (( c       & 1) > 0) a.append("AE");
              if (((c >> 1) & 1) > 0) a.append("WB");
              if (((c >> 2) & 1) > 0) a.append("FL");
              if (((c >> 3) & 1) > 0) a.append("MF");
              if (((c >> 6) & 1) > 0) a.append("Focus");
  
              a.append(" Bracketing");
          } else {
              switch (values[0]) {
                  case 1:
                      a.append("Continuous Shooting");
                      break;
                  case 2:
                      a.append("Exposure Bracketing");
                      break;
                  case 3:
                      a.append("White Balance Bracketing");
                      break;
                  case 4:
                      a.append("Exposure+WB Bracketing");
                      break;
                  default:
                      a.append("Unknown (").append(values[0]).append(")");
                      break;
              }
          }
  
          a.append(", Shot ").append(values[1]);
  
          return a.toString();
      }
  
      /// <summary>
      /// 2 numbers: 1. Mode, 2. Shot number
      /// </summary>
      /// <returns></returns>
      @Nullable
      public String getPanoramaModeDescription()
      {
          int[] values = _directory.getIntArray(TagPanoramaMode);
          if (values == null)
              return null;
  
          if (values.length == 0 || values[0] == 0)
              return "Off";
  
          String a;
          switch (values[0]) {
              case 1:
                  a = "Left to Right";
                  break;
              case 2:
                  a = "Right to Left";
                  break;
              case 3:
                  a = "Bottom to Top";
                  break;
              case 4:
                  a = "Top to Bottom";
                  break;
              default:
                  a = "Unknown (" + values[0] + ")";
                  break;
          }
  
          return String.format("%s, Shot %d", a, values[1]);
      }
  
      @Nullable
      public String getImageQuality2Description()
      {
          return getIndexedDescription(TagImageQuality2, 1,
              "SQ", "HQ", "SHQ", "RAW", "SQ (5)");
      }
  
      @Nullable
      public String getImageStabilizationDescription()
      {
          return getIndexedDescription(TagImageStabilization,
              "Off", "On, Mode 1", "On, Mode 2", "On, Mode 3", "On, Mode 4");
      }
  
      @Nullable
      public String getStackedImageDescription()
      {
          int[] values = _directory.getIntArray(TagStackedImage);
          if (values == null || values.length < 2)
              return null;
  
          int v1 = values[0];
          int v2 = values[1];
  
          if (v1 == 0 && v2 == 0)
              return "No";
          if (v1 == 9 && v2 == 8)
              return "Focus-stacked (8 images)";
  
          return String.format("Unknown (%d %d)", v1, v2);
      }
  
      /// <remarks>
      /// TODO: need better image examples to test this function
      /// </remarks>
      /// <returns></returns>
      @Nullable
      public String getManometerPressureDescription()
      {
          Integer value = _directory.getInteger(TagManometerPressure);
          if (value == null)
              return null;
  
          return String.format("%s kPa", new DecimalFormat("#.##").format(value / 10.0));
      }
  
      /// <remarks>
      /// TODO: need better image examples to test this function
      /// </remarks>
      /// <returns></returns>
      @Nullable
      public String getManometerReadingDescription()
      {
          int[] values = _directory.getIntArray(TagManometerReading);
          if (values == null || values.length < 2)
              return null;
  
          DecimalFormat format = new DecimalFormat("#.##");
          return String.format("%s m, %s ft",
              format.format(values[0] / 10.0),
              format.format(values[1] / 10.0));
      }
  
      @Nullable
      public String getExtendedWBDetectDescription()
      {
          return getIndexedDescription(TagExtendedWBDetect, "Off", "On");
      }
  
      /// <summary>
      /// converted to degrees of clockwise camera rotation
      /// </summary>
      /// <remarks>
      /// TODO: need better image examples to test this function
      /// </remarks>
      /// <returns></returns>
      @Nullable
      public String getRollAngleDescription()
      {
          int[] values = _directory.getIntArray(TagRollAngle);
          if (values == null || values.length < 2)
              return null;
  
          String ret = values[0] != 0
              ? Double.toString(-values[0] / 10.0)
              : "n/a";
  
          return String.format("%s %d", ret, values[1]);
      }
  
      /// <summary>
      /// converted to degrees of upward camera tilt
      /// </summary>
      /// <remarks>
      /// TODO: need better image examples to test this function
      /// </remarks>
      /// <returns></returns>
      @Nullable
      public String getPitchAngleDescription()
      {
          int[] values = _directory.getIntArray(TagPitchAngle);
          if (values == null || values.length < 2)
              return null;
  
          // (second value is 0 if level gauge is off)
          String ret = values[0] != 0
              ? Double.toString(values[0] / 10.0)
              : "n/a";
  
          return String.format("%s %d", ret, values[1]);
      }
  
      @Nullable
      public String getDateTimeUTCDescription()
      {
          Object value = _directory.getObject(TagDateTimeUtc);
          if (value == null)
              return null;
          return value.toString();
      }
  
      @Nullable
      private String getValueMinMaxDescription(int tagId)
      {
          int[] values = _directory.getIntArray(tagId);
          if (values == null || values.length < 3)
              return null;
  
          return String.format("%d (min %d, max %d)", values[0], values[1], values[2]);
      }
  
      @Nullable
      private String getFiltersDescription(int tagId)
      {
          int[] values = _directory.getIntArray(tagId);
          if (values == null || values.length == 0)
              return null;
  
          StringBuilder sb = new StringBuilder();
          for (int i = 0; i < values.length; i++) {
              if (i == 0)
                  sb.append(_filters.containsKey(values[i]) ? _filters.get(values[i]) : "[unknown]");
              else
                  sb.append(values[i]);
              sb.append("; ");
          }
  
          return sb.substring(0, sb.length() - 2);
      }
  
      private static final HashMap<Integer, String> _toneLevelType = new HashMap<Integer, String>();
      // ArtFilter, ArtFilterEffect and MagicFilter values
      private static final HashMap<Integer, String> _filters = new HashMap<Integer, String>();
  
      static {
          _filters.put(0, "Off");
          _filters.put(1, "Soft Focus");
          _filters.put(2, "Pop Art");
          _filters.put(3, "Pale & Light Color");
          _filters.put(4, "Light Tone");
          _filters.put(5, "Pin Hole");
          _filters.put(6, "Grainy Film");
          _filters.put(9, "Diorama");
          _filters.put(10, "Cross Process");
          _filters.put(12, "Fish Eye");
          _filters.put(13, "Drawing");
          _filters.put(14, "Gentle Sepia");
          _filters.put(15, "Pale & Light Color II");
          _filters.put(16, "Pop Art II");
          _filters.put(17, "Pin Hole II");
          _filters.put(18, "Pin Hole III");
          _filters.put(19, "Grainy Film II");
          _filters.put(20, "Dramatic Tone");
          _filters.put(21, "Punk");
          _filters.put(22, "Soft Focus 2");
          _filters.put(23, "Sparkle");
          _filters.put(24, "Watercolor");
          _filters.put(25, "Key Line");
          _filters.put(26, "Key Line II");
          _filters.put(27, "Miniature");
          _filters.put(28, "Reflection");
          _filters.put(29, "Fragmented");
          _filters.put(31, "Cross Process II");
          _filters.put(32, "Dramatic Tone II");
          _filters.put(33, "Watercolor I");
          _filters.put(34, "Watercolor II");
          _filters.put(35, "Diorama II");
          _filters.put(36, "Vintage");
          _filters.put(37, "Vintage II");
          _filters.put(38, "Vintage III");
          _filters.put(39, "Partial Color");
          _filters.put(40, "Partial Color II");
          _filters.put(41, "Partial Color III");
  
          _toneLevelType.put(0, "0");
          _toneLevelType.put(-31999, "Highlights ");
          _toneLevelType.put(-31998, "Shadows ");
          _toneLevelType.put(-31997, "Midtones ");
      }
  }