001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.tiff.taginfos;
018
019import java.io.UnsupportedEncodingException;
020import java.nio.ByteOrder;
021import java.nio.charset.StandardCharsets;
022
023import org.apache.commons.imaging.ImageReadException;
024import org.apache.commons.imaging.ImageWriteException;
025import org.apache.commons.imaging.common.BinaryFunctions;
026import org.apache.commons.imaging.formats.tiff.TiffField;
027import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
028import org.apache.commons.imaging.formats.tiff.fieldtypes.FieldType;
029import org.apache.commons.imaging.internal.Debug;
030
031/**
032 * Used by some GPS tags and the EXIF user comment tag,
033 * this badly documented value is meant to contain
034 * the text encoding in the first 8 bytes followed by
035 * the non-null-terminated text in an unknown byte order.
036 */
037public final class TagInfoGpsText extends TagInfo {
038    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_ASCII = new TextEncoding(
039            new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, },
040            "US-ASCII"); // ITU-T T.50 IA5
041    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_JIS = new TextEncoding(
042            new byte[] { 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, },
043            "JIS"); // JIS X208-1990
044    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_LE = new TextEncoding(
045            new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00},
046            "UTF-16LE"); // Unicode Standard
047    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_BE = new TextEncoding(
048            new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00},
049            "UTF-16BE"); // Unicode Standard
050    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNDEFINED = new TextEncoding(
051            new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
052            // Try to interpret an undefined text as ISO-8859-1 (Latin)
053            "ISO-8859-1"); // Undefined
054    private static final TagInfoGpsText.TextEncoding[] TEXT_ENCODINGS = {
055            TEXT_ENCODING_ASCII, //
056            TEXT_ENCODING_JIS, //
057            TEXT_ENCODING_UNICODE_LE, //
058            TEXT_ENCODING_UNICODE_BE, //
059            TEXT_ENCODING_UNDEFINED, //
060    };
061
062    public TagInfoGpsText(final String name, final int tag,
063            final TiffDirectoryType exifDirectory) {
064        super(name, tag, FieldType.UNDEFINED, LENGTH_UNKNOWN, exifDirectory);
065    }
066
067    @Override
068    public boolean isText() {
069        return true;
070    }
071
072    private static final class TextEncoding {
073        final byte[] prefix;
074        public final String encodingName;
075
076        TextEncoding(final byte[] prefix, final String encodingName) {
077            this.prefix = prefix;
078            this.encodingName = encodingName;
079        }
080    }
081
082    @Override
083    public byte[] encodeValue(final FieldType fieldType, final Object value, final ByteOrder byteOrder)
084            throws ImageWriteException {
085        if (!(value instanceof String)) {
086            throw new ImageWriteException("GPS text value not String", value);
087        }
088        final String s = (String) value;
089
090        try {
091            // try ASCII, with NO prefix.
092            final byte[] asciiBytes = s.getBytes(TEXT_ENCODING_ASCII.encodingName);
093            final String decodedAscii = new String(asciiBytes, TEXT_ENCODING_ASCII.encodingName);
094            if (decodedAscii.equals(s)) {
095                // no unicode/non-ascii values.
096                final byte[] result = new byte[asciiBytes.length
097                        + TEXT_ENCODING_ASCII.prefix.length];
098                System.arraycopy(TEXT_ENCODING_ASCII.prefix, 0, result, 0,
099                        TEXT_ENCODING_ASCII.prefix.length);
100                System.arraycopy(asciiBytes, 0, result,
101                        TEXT_ENCODING_ASCII.prefix.length, asciiBytes.length);
102                return result;
103            }
104            // use Unicode
105            final TextEncoding encoding;
106            if (byteOrder == ByteOrder.BIG_ENDIAN) {
107                encoding = TEXT_ENCODING_UNICODE_BE;
108            } else {
109                encoding = TEXT_ENCODING_UNICODE_LE;
110            }
111            final byte[] unicodeBytes = s.getBytes(encoding.encodingName);
112            final byte[] result = new byte[unicodeBytes.length + encoding.prefix.length];
113            System.arraycopy(encoding.prefix, 0, result, 0, encoding.prefix.length);
114            System.arraycopy(unicodeBytes, 0, result, encoding.prefix.length, unicodeBytes.length);
115            return result;
116        } catch (final UnsupportedEncodingException e) {
117            throw new ImageWriteException(e.getMessage(), e);
118        }
119    }
120
121    @Override
122    public String getValue(final TiffField entry) throws ImageReadException {
123        if (entry.getFieldType() == FieldType.ASCII) {
124            final Object object = FieldType.ASCII.getValue(entry);
125            if (object instanceof String) {
126                return (String) object;
127            } else if (object instanceof String[]) {
128                // Use of arrays with the ASCII type
129                // should be extremely rare, and use of
130                // ASCII type in GPS fields should be
131                // forbidden. So assume the 2 never happen
132                // together and return incomplete strings if they do.
133                return ((String[]) object)[0];
134            } else {
135                throw new ImageReadException("Unexpected ASCII type decoded");
136            }
137        } else if (entry.getFieldType() == FieldType.UNDEFINED) {
138            /* later */
139        } else if (entry.getFieldType() == FieldType.BYTE) {
140            /* later */
141        } else {
142            Debug.debug("entry.type: " + entry.getFieldType());
143            Debug.debug("entry.directoryType: " + entry.getDirectoryType());
144            Debug.debug("entry.type: " + entry.getDescriptionWithoutValue());
145            Debug.debug("entry.type: " + entry.getFieldType());
146            throw new ImageReadException("GPS text field not encoded as bytes.");
147        }
148
149        final byte[] bytes = entry.getByteArrayValue();
150        if (bytes.length < 8) {
151            // try ASCII, with NO prefix.
152            return new String(bytes, StandardCharsets.US_ASCII);
153        }
154
155        for (final TextEncoding encoding : TEXT_ENCODINGS) {
156            if (BinaryFunctions.compareBytes(bytes, 0, encoding.prefix, 0,
157                    encoding.prefix.length)) {
158                try {
159                    final String decodedString = new String(
160                            bytes, encoding.prefix.length,
161                            bytes.length - encoding.prefix.length,
162                            encoding.encodingName);
163                    final byte[] reEncodedBytes = decodedString.getBytes(
164                            encoding.encodingName);
165                    if (BinaryFunctions.compareBytes(bytes, encoding.prefix.length,
166                            reEncodedBytes, 0,
167                            reEncodedBytes.length)) {
168                        return decodedString;
169                    }
170                } catch (final UnsupportedEncodingException e) {
171                    throw new ImageReadException(e.getMessage(), e);
172                }
173            }
174        }
175
176        // try ASCII, with NO prefix.
177        return new String(bytes, StandardCharsets.US_ASCII);
178    }
179}