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}