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.ImagingException;
024import org.apache.commons.imaging.common.Allocator;
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.AbstractFieldType;
029import org.apache.commons.imaging.internal.Debug;
030
031/**
032 * 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
033 * non-null-terminated text in an unknown byte order.
034 */
035public final class TagInfoGpsText extends TagInfo {
036
037    private static final class TextEncoding {
038        final byte[] prefix;
039        public final String encodingName;
040
041        TextEncoding(final byte[] prefix, final String encodingName) {
042            this.prefix = prefix;
043            this.encodingName = encodingName;
044        }
045    }
046
047    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_ASCII = new TextEncoding(new byte[] { 0x41, 0x53, 0x43, 0x49, 0x49, 0x00, 0x00, 0x00, },
048            StandardCharsets.US_ASCII.name()); // ITU-T T.50 IA5
049    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_JIS = new TextEncoding(new byte[] { 0x4A, 0x49, 0x53, 0x00, 0x00, 0x00, 0x00, 0x00, },
050            "JIS"); // JIS X208-1990
051    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_LE = new TextEncoding(new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 },
052            StandardCharsets.UTF_16LE.name()); // Unicode Standard
053    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNICODE_BE = new TextEncoding(new byte[] { 0x55, 0x4E, 0x49, 0x43, 0x4F, 0x44, 0x45, 0x00 },
054            StandardCharsets.UTF_16BE.name()); // Unicode Standard
055    private static final TagInfoGpsText.TextEncoding TEXT_ENCODING_UNDEFINED = new TextEncoding(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
056            // Try to interpret an undefined text as ISO-8859-1 (Latin)
057            StandardCharsets.ISO_8859_1.name()); // Undefined
058
059    private static final TagInfoGpsText.TextEncoding[] TEXT_ENCODINGS = { TEXT_ENCODING_ASCII, //
060            TEXT_ENCODING_JIS, //
061            TEXT_ENCODING_UNICODE_LE, //
062            TEXT_ENCODING_UNICODE_BE, //
063            TEXT_ENCODING_UNDEFINED, //
064    };
065
066    public TagInfoGpsText(final String name, final int tag, final TiffDirectoryType exifDirectory) {
067        super(name, tag, AbstractFieldType.UNDEFINED, LENGTH_UNKNOWN, exifDirectory);
068    }
069
070    @Override
071    public byte[] encodeValue(final AbstractFieldType abstractFieldType, final Object value, final ByteOrder byteOrder) throws ImagingException {
072        if (!(value instanceof String)) {
073            throw new ImagingException("GPS text value not String", value);
074        }
075        final String s = (String) value;
076
077        try {
078            // try ASCII, with NO prefix.
079            final byte[] asciiBytes = s.getBytes(TEXT_ENCODING_ASCII.encodingName);
080            final String decodedAscii = new String(asciiBytes, TEXT_ENCODING_ASCII.encodingName);
081            if (decodedAscii.equals(s)) {
082                // no unicode/non-ascii values.
083                final byte[] result = Allocator.byteArray(asciiBytes.length + TEXT_ENCODING_ASCII.prefix.length);
084                System.arraycopy(TEXT_ENCODING_ASCII.prefix, 0, result, 0, TEXT_ENCODING_ASCII.prefix.length);
085                System.arraycopy(asciiBytes, 0, result, TEXT_ENCODING_ASCII.prefix.length, asciiBytes.length);
086                return result;
087            }
088            // use Unicode
089            final TextEncoding encoding;
090            if (byteOrder == ByteOrder.BIG_ENDIAN) {
091                encoding = TEXT_ENCODING_UNICODE_BE;
092            } else {
093                encoding = TEXT_ENCODING_UNICODE_LE;
094            }
095            final byte[] unicodeBytes = s.getBytes(encoding.encodingName);
096            final byte[] result = Allocator.byteArray(unicodeBytes.length + encoding.prefix.length);
097            System.arraycopy(encoding.prefix, 0, result, 0, encoding.prefix.length);
098            System.arraycopy(unicodeBytes, 0, result, encoding.prefix.length, unicodeBytes.length);
099            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}