View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.tiff.taginfos;
18  
19  import java.io.UnsupportedEncodingException;
20  import java.nio.ByteOrder;
21  import java.nio.charset.StandardCharsets;
22  
23  import org.apache.commons.imaging.ImagingException;
24  import org.apache.commons.imaging.common.Allocator;
25  import org.apache.commons.imaging.common.BinaryFunctions;
26  import org.apache.commons.imaging.formats.tiff.TiffField;
27  import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
28  import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
29  import org.apache.commons.imaging.internal.Debug;
30  
31  /**
32   * Used by some GPS tags and the EXIF user comment tag, this badly documented value is meant to contain the text encoding in the first 8 bytes followed by the
33   * non-null-terminated text in an unknown byte order.
34   */
35  public final class TagInfoGpsText extends TagInfo {
36  
37      private static final class TextEncoding {
38          final byte[] prefix;
39          public final String encodingName;
40  
41          TextEncoding(final byte[] prefix, final String encodingName) {
42              this.prefix = prefix;
43              this.encodingName = encodingName;
44          }
45      }
46  
47      private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_ASCII = new TextEncoding(new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, },
48              StandardCharsets.US_ASCII.name()); // ITU-T T.50 IA5
49      private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_JIS = new TextEncoding(new byte[] { 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, },
50              "JIS"); // JIS X208-1990
51      private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_LE = new TextEncoding(new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 },
52              StandardCharsets.UTF_16LE.name()); // Unicode Standard
53      private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_BE = new TextEncoding(new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 },
54              StandardCharsets.UTF_16BE.name()); // Unicode Standard
55      private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNDEFINED = new TextEncoding(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
56              // Try to interpret an undefined text as ISO-8859-1 (Latin)
57              StandardCharsets.ISO_8859_1.name()); // Undefined
58  
59      private static final TagInfoGpsText.TextEncoding[] TEXT_ENCODINGS = { TEXT_ENCODING_ASCII, //
60              TEXT_ENCODING_JIS, //
61              TEXT_ENCODING_UNICODE_LE, //
62              TEXT_ENCODING_UNICODE_BE, //
63              TEXT_ENCODING_UNDEFINED, //
64      };
65  
66      public TagInfoGpsText(final String name, final int tag, final TiffDirectoryType exifDirectory) {
67          super(name, tag, AbstractFieldType.UNDEFINED, LENGTH_UNKNOWN, exifDirectory);
68      }
69  
70      @Override
71      public byte[] encodeValue(final AbstractFieldType abstractFieldType, final Object value, final ByteOrder byteOrder) throws ImagingException {
72          if (!(value instanceof String)) {
73              throw new ImagingException("GPS text value not String", value);
74          }
75          final String s = (String) value;
76  
77          try {
78              // try ASCII, with NO prefix.
79              final byte[] asciiBytes = s.getBytes(TEXT_ENCODING_ASCII.encodingName);
80              final String decodedAscii = new String(asciiBytes, TEXT_ENCODING_ASCII.encodingName);
81              if (decodedAscii.equals(s)) {
82                  // no unicode/non-ascii values.
83                  final byte[] result = Allocator.byteArray(asciiBytes.length + TEXT_ENCODING_ASCII.prefix.length);
84                  System.arraycopy(TEXT_ENCODING_ASCII.prefix, 0, result, 0, TEXT_ENCODING_ASCII.prefix.length);
85                  System.arraycopy(asciiBytes, 0, result, TEXT_ENCODING_ASCII.prefix.length, asciiBytes.length);
86                  return result;
87              }
88              // use Unicode
89              final TextEncoding encoding;
90              if (byteOrder == ByteOrder.BIG_ENDIAN) {
91                  encoding = TEXT_ENCODING_UNICODE_BE;
92              } else {
93                  encoding = TEXT_ENCODING_UNICODE_LE;
94              }
95              final byte[] unicodeBytes = s.getBytes(encoding.encodingName);
96              final byte[] result = Allocator.byteArray(unicodeBytes.length + encoding.prefix.length);
97              System.arraycopy(encoding.prefix, 0, result, 0, encoding.prefix.length);
98              System.arraycopy(unicodeBytes, 0, result, encoding.prefix.length, unicodeBytes.length);
99              return result;
100         } catch (final UnsupportedEncodingException e) {
101             throw new ImagingException(e.getMessage(), e);
102         }
103     }
104 
105     @Override
106     public String getValue(final TiffField entry) throws ImagingException {
107         if (entry.getFieldType() == AbstractFieldType.ASCII) {
108             final Object object = AbstractFieldType.ASCII.getValue(entry);
109             if (object instanceof String) {
110                 return (String) object;
111             }
112             if (object instanceof String[]) {
113                 // Use of arrays with the ASCII type
114                 // should be extremely rare, and use of
115                 // ASCII type in GPS fields should be
116                 // forbidden. So assume the 2 never happen
117                 // together and return incomplete strings if they do.
118                 return ((String[]) object)[0];
119             }
120             throw new ImagingException("Unexpected ASCII type decoded");
121         }
122         if (entry.getFieldType() != AbstractFieldType.UNDEFINED && entry.getFieldType() != AbstractFieldType.BYTE) {
123             Debug.debug("entry.type: " + entry.getFieldType());
124             Debug.debug("entry.directoryType: " + entry.getDirectoryType());
125             Debug.debug("entry.type: " + entry.getDescriptionWithoutValue());
126             Debug.debug("entry.type: " + entry.getFieldType());
127             throw new ImagingException("GPS text field not encoded as bytes.");
128         }
129 
130         final byte[] bytes = entry.getByteArrayValue();
131         if (bytes.length < 8) {
132             // try ASCII, with NO prefix.
133             return new String(bytes, StandardCharsets.US_ASCII);
134         }
135 
136         for (final TextEncoding encoding : TEXT_ENCODINGS) {
137             if (BinaryFunctions.compareBytes(bytes, 0, encoding.prefix, 0, encoding.prefix.length)) {
138                 try {
139                     final String decodedString = new String(bytes, encoding.prefix.length, bytes.length - encoding.prefix.length, encoding.encodingName);
140                     final byte[] reEncodedBytes = decodedString.getBytes(encoding.encodingName);
141                     if (BinaryFunctions.compareBytes(bytes, encoding.prefix.length, reEncodedBytes, 0, reEncodedBytes.length)) {
142                         return decodedString;
143                     }
144                 } catch (final UnsupportedEncodingException e) {
145                     throw new ImagingException(e.getMessage(), e);
146                 }
147             }
148         }
149 
150         // try ASCII, with NO prefix.
151         return new String(bytes, StandardCharsets.US_ASCII);
152     }
153 
154     @Override
155     public boolean isText() {
156         return true;
157     }
158 }