    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}