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 } 128 if (object instanceof String[]) { 129 // Use of arrays with the ASCII type 130 // should be extremely rare, and use of 131 // ASCII type in GPS fields should be 132 // forbidden. So assume the 2 never happen 133 // together and return incomplete strings if they do. 134 return ((String[]) object)[0]; 135 } 136 throw new ImageReadException("Unexpected ASCII type decoded"); 137 } 138 if (entry.getFieldType() == FieldType.UNDEFINED) { 139 /* later */ 140 } else if (entry.getFieldType() == FieldType.BYTE) { 141 /* later */ 142 } else { 143 Debug.debug("entry.type: " + entry.getFieldType()); 144 Debug.debug("entry.directoryType: " + entry.getDirectoryType()); 145 Debug.debug("entry.type: " + entry.getDescriptionWithoutValue()); 146 Debug.debug("entry.type: " + entry.getFieldType()); 147 throw new ImageReadException("GPS text field not encoded as bytes."); 148 } 149 150 final byte[] bytes = entry.getByteArrayValue(); 151 if (bytes.length < 8) { 152 // try ASCII, with NO prefix. 153 return new String(bytes, StandardCharsets.US_ASCII); 154 } 155 156 for (final TextEncoding encoding : TEXT_ENCODINGS) { 157 if (BinaryFunctions.compareBytes(bytes, 0, encoding.prefix, 0, 158 encoding.prefix.length)) { 159 try { 160 final String decodedString = new String( 161 bytes, encoding.prefix.length, 162 bytes.length - encoding.prefix.length, 163 encoding.encodingName); 164 final byte[] reEncodedBytes = decodedString.getBytes( 165 encoding.encodingName); 166 if (BinaryFunctions.compareBytes(bytes, encoding.prefix.length, 167 reEncodedBytes, 0, 168 reEncodedBytes.length)) { 169 return decodedString; 170 } 171 } catch (final UnsupportedEncodingException e) { 172 throw new ImageReadException(e.getMessage(), e); 173 } 174 } 175 } 176 177 // try ASCII, with NO prefix. 178 return new String(bytes, StandardCharsets.US_ASCII); 179 } 180}