    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}