- Timestamp:
- 2015-03-10T01:17:39+01:00 (10 years ago)
- Location:
- trunk
- Files:
-
- 61 added
- 31 deleted
- 49 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/CONTRIBUTION
r7937 r8132 35 35 36 36 The jpeg metadata extraction code is from Drew Noakes 37 (http ://code.google.com/p/metadata-extractor/) and licensed37 (https://github.com/drewnoakes/metadata-extractor) and licensed 38 38 with Apache license version 2.0. 39 39 -
trunk/src/com/drew/imaging/ImageProcessingException.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging; … … 26 26 /** 27 27 * An exception class thrown upon an unexpected condition that was fatal for the processing of an image. 28 * 29 * @author Drew Noakes http://drewnoakes.com 28 * 29 * @author Drew Noakes https://drewnoakes.com 30 30 */ 31 31 public class ImageProcessingException extends CompoundException -
trunk/src/com/drew/imaging/PhotographicConversions.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging; … … 24 24 * Contains helper methods that perform photographic conversions. 25 25 * 26 * @author Drew Noakes http://drewnoakes.com 26 * @author Drew Noakes https://drewnoakes.com 27 27 */ 28 28 public final class PhotographicConversions -
trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging.jpeg; 22 22 23 import com.drew.lang.ByteArrayReader;24 import com.drew.lang.annotations.NotNull;25 import com.drew.metadata.Metadata;26 import com.drew.metadata.exif.ExifReader;27 import com.drew.metadata.iptc.IptcReader;28 import com.drew.metadata.jpeg.JpegCommentReader;29 import com.drew.metadata.jpeg.JpegDirectory;30 import com.drew.metadata.jpeg.JpegReader;31 32 23 import java.io.File; 24 import java.io.FileInputStream; 33 25 import java.io.IOException; 34 26 import java.io.InputStream; 27 import java.util.Arrays; 28 import java.util.HashSet; 29 import java.util.Set; 30 31 import com.drew.lang.StreamReader; 32 import com.drew.lang.annotations.NotNull; 33 import com.drew.lang.annotations.Nullable; 34 import com.drew.metadata.Metadata; 35 //import com.drew.metadata.adobe.AdobeJpegReader; 36 import com.drew.metadata.exif.ExifReader; 37 //import com.drew.metadata.icc.IccReader; 38 import com.drew.metadata.iptc.IptcReader; 39 //import com.drew.metadata.jfif.JfifReader; 40 import com.drew.metadata.jpeg.JpegCommentReader; 41 import com.drew.metadata.jpeg.JpegReader; 42 //import com.drew.metadata.photoshop.PhotoshopReader; 43 //import com.drew.metadata.xmp.XmpReader; 35 44 36 45 /** 37 * Obtains all available metadata from J pegformatted files.46 * Obtains all available metadata from JPEG formatted files. 38 47 * 39 * @author Drew Noakes http://drewnoakes.com 48 * @author Drew Noakes https://drewnoakes.com 40 49 */ 41 50 public class JpegMetadataReader 42 51 { 43 // TODO investigate supporting javax.imageio 44 // public static Metadata readMetadata(IIOMetadata metadata) throws JpegProcessingException {} 45 // public static Metadata readMetadata(ImageInputStream in) throws JpegProcessingException{} 46 // public static Metadata readMetadata(IIOImage image) throws JpegProcessingException{} 47 // public static Metadata readMetadata(ImageReader reader) throws JpegProcessingException{} 52 public static final Iterable<JpegSegmentMetadataReader> ALL_READERS = Arrays.asList( 53 new JpegReader(), 54 new JpegCommentReader(), 55 //new JfifReader(), 56 new ExifReader(), 57 //new XmpReader(), 58 //new IccReader(), 59 //new PhotoshopReader(), 60 new IptcReader()//, 61 //new AdobeJpegReader() 62 ); 48 63 49 64 @NotNull 50 public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException 65 public static Metadata readMetadata(@NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException 51 66 { 52 return readMetadata(inputStream, true); 67 Metadata metadata = new Metadata(); 68 process(metadata, inputStream, readers); 69 return metadata; 53 70 } 54 71 55 72 @NotNull 56 public static Metadata readMetadata(@NotNull InputStream inputStream , final boolean waitForBytes) throws JpegProcessingException73 public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException, IOException 57 74 { 58 JpegSegmentReader segmentReader = new JpegSegmentReader(inputStream, waitForBytes); 59 return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData()); 75 return readMetadata(inputStream, null); 76 } 77 78 @NotNull 79 public static Metadata readMetadata(@NotNull File file, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException 80 { 81 InputStream inputStream = null; 82 try 83 { 84 inputStream = new FileInputStream(file); 85 return readMetadata(inputStream, readers); 86 } finally { 87 if (inputStream != null) 88 inputStream.close(); 89 } 60 90 } 61 91 … … 63 93 public static Metadata readMetadata(@NotNull File file) throws JpegProcessingException, IOException 64 94 { 65 JpegSegmentReader segmentReader = new JpegSegmentReader(file); 66 return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData()); 95 return readMetadata(file, null); 67 96 } 68 97 69 @NotNull 70 public static Metadata extractMetadataFromJpegSegmentReader(@NotNull JpegSegmentData segmentReader) 98 public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream) throws JpegProcessingException, IOException 71 99 { 72 final Metadata metadata = new Metadata(); 100 process(metadata, inputStream, null); 101 } 73 102 74 // Loop through looking for all SOFn segments. When we find one, we know what type of compression 75 // was used for the JPEG, and we can process the JPEG metadata in the segment too. 76 for (byte i = 0; i < 16; i++) { 77 // There are no SOF4 or SOF12 segments, so don't bother 78 if (i == 4 || i == 12) 79 continue; 80 // Should never have more than one SOFn for a given 'n'. 81 byte[] jpegSegment = segmentReader.getSegment((byte)(JpegSegmentReader.SEGMENT_SOF0 + i)); 82 if (jpegSegment == null) 83 continue; 84 JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class); 85 directory.setInt(JpegDirectory.TAG_JPEG_COMPRESSION_TYPE, i); 86 new JpegReader().extract(new ByteArrayReader(jpegSegment), metadata); 87 break; 88 } 103 public static void process(@NotNull Metadata metadata, @NotNull InputStream inputStream, @Nullable Iterable<JpegSegmentMetadataReader> readers) throws JpegProcessingException, IOException 104 { 105 if (readers == null) 106 readers = ALL_READERS; 89 107 90 // There should never be more than one COM segment. 91 byte[] comSegment = segmentReader.getSegment(JpegSegmentReader.SEGMENT_COM); 92 if (comSegment != null) 93 new JpegCommentReader().extract(new ByteArrayReader(comSegment), metadata); 94 95 // Loop through all APP1 segments, checking the leading bytes to identify the format of each. 96 for (byte[] app1Segment : segmentReader.getSegments(JpegSegmentReader.SEGMENT_APP1)) { 97 if (app1Segment.length > 3 && "EXIF".equalsIgnoreCase(new String(app1Segment, 0, 4))) 98 new ExifReader().extract(new ByteArrayReader(app1Segment), metadata); 99 100 //if (app1Segment.length > 27 && "http://ns.adobe.com/xap/1.0/".equalsIgnoreCase(new String(app1Segment, 0, 28))) 101 // new XmpReader().extract(new ByteArrayReader(app1Segment), metadata); 102 } 103 104 // Loop through all APPD segments, checking the leading bytes to identify the format of each. 105 for (byte[] appdSegment : segmentReader.getSegments(JpegSegmentReader.SEGMENT_APPD)) { 106 if (appdSegment.length > 12 && "Photoshop 3.0".compareTo(new String(appdSegment, 0, 13))==0) { 107 //new PhotoshopReader().extract(new ByteArrayReader(appdSegment), metadata); 108 } else { 109 // TODO might be able to check for a leading 0x1c02 for IPTC data... 110 new IptcReader().extract(new ByteArrayReader(appdSegment), metadata); 108 Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>(); 109 for (JpegSegmentMetadataReader reader : readers) { 110 for (JpegSegmentType type : reader.getSegmentTypes()) { 111 segmentTypes.add(type); 111 112 } 112 113 } 113 114 114 return metadata; 115 JpegSegmentData segmentData = JpegSegmentReader.readSegments(new StreamReader(inputStream), segmentTypes); 116 117 processJpegSegmentData(metadata, readers, segmentData); 118 } 119 120 public static void processJpegSegmentData(Metadata metadata, Iterable<JpegSegmentMetadataReader> readers, JpegSegmentData segmentData) 121 { 122 // Pass the appropriate byte arrays to each reader. 123 for (JpegSegmentMetadataReader reader : readers) { 124 for (JpegSegmentType segmentType : reader.getSegmentTypes()) { 125 for (byte[] segmentBytes : segmentData.getSegments(segmentType)) { 126 if (reader.canProcess(segmentBytes, segmentType)) { 127 reader.extract(segmentBytes, metadata, segmentType); 128 } 129 } 130 } 131 } 115 132 } 116 133 … … 120 137 } 121 138 } 122 -
trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging.jpeg; … … 25 25 26 26 /** 27 * An exception class thrown upon unexpected and fatal conditions while processing a J pegfile.27 * An exception class thrown upon unexpected and fatal conditions while processing a JPEG file. 28 28 * 29 * @author Drew Noakes http://drewnoakes.com 29 * @author Drew Noakes https://drewnoakes.com 30 30 */ 31 31 public class JpegProcessingException extends ImageProcessingException -
trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging.jpeg; … … 24 24 import com.drew.lang.annotations.Nullable; 25 25 26 import java.io.*; 27 import java.util.ArrayList; 28 import java.util.HashMap; 29 import java.util.List; 26 import java.util.*; 30 27 31 28 /** 32 * Holds a collection of Jpeg data segments. This need not necessarily be all segments 33 * within the Jpeg. For example, it may be convenient to store only the non-image 34 * segments when analysing (or serializing) metadata. 35 * <p/> 36 * Segments are keyed via their segment marker (a byte). Where multiple segments use the 37 * same segment marker, they will all be stored and available. 38 * 39 * @author Drew Noakes http://drewnoakes.com 29 * Holds a collection of JPEG data segments. This need not necessarily be all segments 30 * within the JPEG. For example, it may be convenient to store only the non-image 31 * segments when analysing metadata. 32 * <p> 33 * Segments are keyed via their {@link JpegSegmentType}. Where multiple segments use the 34 * same segment type, they will all be stored and available. 35 * <p> 36 * Each segment type may contain multiple entries. Conceptually the model is: 37 * <code>Map<JpegSegmentType, Collection<byte[]>></code>. This class provides 38 * convenience methods around that structure. 39 * 40 * @author Drew Noakes https://drewnoakes.com 40 41 */ 41 public class JpegSegmentData implements Serializable42 public class JpegSegmentData 42 43 { 43 private static final long serialVersionUID = 7110175216435025451L; 44 45 /** A map of byte[], keyed by the segment marker */ 44 // TODO key this on JpegSegmentType rather than Byte, and hopefully lose much of the use of 'byte' with this class 46 45 @NotNull 47 46 private final HashMap<Byte, List<byte[]>> _segmentDataMap = new HashMap<Byte, List<byte[]>>(10); … … 49 48 /** 50 49 * Adds segment bytes to the collection. 51 * @param segmentMarker 52 * @param segmentBytes 53 */ 54 @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection" }) 55 public void addSegment(byte segmentMarker, @NotNull byte[] segmentBytes) 56 { 57 final List<byte[]> segmentList = getOrCreateSegmentList(segmentMarker); 58 segmentList.add(segmentBytes); 59 } 60 61 /** 62 * Gets the first Jpeg segment data for the specified marker. 63 * @param segmentMarker the byte identifier for the desired segment 50 * 51 * @param segmentType the type of the segment being added 52 * @param segmentBytes the byte array holding data for the segment being added 53 */ 54 @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) 55 public void addSegment(byte segmentType, @NotNull byte[] segmentBytes) 56 { 57 getOrCreateSegmentList(segmentType).add(segmentBytes); 58 } 59 60 /** 61 * Gets the set of JPEG segment type identifiers. 62 */ 63 public Iterable<JpegSegmentType> getSegmentTypes() 64 { 65 Set<JpegSegmentType> segmentTypes = new HashSet<JpegSegmentType>(); 66 67 for (Byte segmentTypeByte : _segmentDataMap.keySet()) 68 { 69 JpegSegmentType segmentType = JpegSegmentType.fromByte(segmentTypeByte); 70 if (segmentType == null) { 71 throw new IllegalStateException("Should not have a segmentTypeByte that is not in the enum: " + Integer.toHexString(segmentTypeByte)); 72 } 73 segmentTypes.add(segmentType); 74 } 75 76 return segmentTypes; 77 } 78 79 /** 80 * Gets the first JPEG segment data for the specified type. 81 * 82 * @param segmentType the JpegSegmentType for the desired segment 64 83 * @return a byte[] containing segment data or null if no data exists for that segment 65 84 */ 66 85 @Nullable 67 public byte[] getSegment(byte segmentMarker) 68 { 69 return getSegment(segmentMarker, 0); 70 } 71 72 /** 73 * Gets segment data for a specific occurrence and marker. Use this method when more than one occurrence 74 * of segment data for a given marker exists. 75 * @param segmentMarker identifies the required segment 76 * @param occurrence the zero-based index of the occurrence 77 * @return the segment data as a byte[], or null if no segment exists for the marker & occurrence 78 */ 79 @Nullable 80 public byte[] getSegment(byte segmentMarker, int occurrence) 81 { 82 final List<byte[]> segmentList = getSegmentList(segmentMarker); 83 84 if (segmentList==null || segmentList.size()<=occurrence) 85 return null; 86 else 87 return segmentList.get(occurrence); 88 } 89 90 /** 91 * Returns all instances of a given Jpeg segment. If no instances exist, an empty sequence is returned. 92 * 93 * @param segmentMarker a number which identifies the type of Jpeg segment being queried 94 * @return zero or more byte arrays, each holding the data of a Jpeg segment 95 */ 96 @NotNull 97 public Iterable<byte[]> getSegments(byte segmentMarker) 98 { 99 final List<byte[]> segmentList = getSegmentList(segmentMarker); 100 return segmentList==null ? new ArrayList<byte[]>() : segmentList; 101 } 102 103 @Nullable 104 public List<byte[]> getSegmentList(byte segmentMarker) 105 { 106 return _segmentDataMap.get(Byte.valueOf(segmentMarker)); 107 } 108 109 @NotNull 110 private List<byte[]> getOrCreateSegmentList(byte segmentMarker) 86 public byte[] getSegment(byte segmentType) 87 { 88 return getSegment(segmentType, 0); 89 } 90 91 /** 92 * Gets the first JPEG segment data for the specified type. 93 * 94 * @param segmentType the JpegSegmentType for the desired segment 95 * @return a byte[] containing segment data or null if no data exists for that segment 96 */ 97 @Nullable 98 public byte[] getSegment(@NotNull JpegSegmentType segmentType) 99 { 100 return getSegment(segmentType.byteValue, 0); 101 } 102 103 /** 104 * Gets segment data for a specific occurrence and type. Use this method when more than one occurrence 105 * of segment data for a given type exists. 106 * 107 * @param segmentType identifies the required segment 108 * @param occurrence the zero-based index of the occurrence 109 * @return the segment data as a byte[], or null if no segment exists for the type & occurrence 110 */ 111 @Nullable 112 public byte[] getSegment(@NotNull JpegSegmentType segmentType, int occurrence) 113 { 114 return getSegment(segmentType.byteValue, occurrence); 115 } 116 117 /** 118 * Gets segment data for a specific occurrence and type. Use this method when more than one occurrence 119 * of segment data for a given type exists. 120 * 121 * @param segmentType identifies the required segment 122 * @param occurrence the zero-based index of the occurrence 123 * @return the segment data as a byte[], or null if no segment exists for the type & occurrence 124 */ 125 @Nullable 126 public byte[] getSegment(byte segmentType, int occurrence) 127 { 128 final List<byte[]> segmentList = getSegmentList(segmentType); 129 130 return segmentList != null && segmentList.size() > occurrence 131 ? segmentList.get(occurrence) 132 : null; 133 } 134 135 /** 136 * Returns all instances of a given JPEG segment. If no instances exist, an empty sequence is returned. 137 * 138 * @param segmentType a number which identifies the type of JPEG segment being queried 139 * @return zero or more byte arrays, each holding the data of a JPEG segment 140 */ 141 @NotNull 142 public Iterable<byte[]> getSegments(@NotNull JpegSegmentType segmentType) 143 { 144 return getSegments(segmentType.byteValue); 145 } 146 147 /** 148 * Returns all instances of a given JPEG segment. If no instances exist, an empty sequence is returned. 149 * 150 * @param segmentType a number which identifies the type of JPEG segment being queried 151 * @return zero or more byte arrays, each holding the data of a JPEG segment 152 */ 153 @NotNull 154 public Iterable<byte[]> getSegments(byte segmentType) 155 { 156 final List<byte[]> segmentList = getSegmentList(segmentType); 157 return segmentList == null ? new ArrayList<byte[]>() : segmentList; 158 } 159 160 @Nullable 161 private List<byte[]> getSegmentList(byte segmentType) 162 { 163 return _segmentDataMap.get(segmentType); 164 } 165 166 @NotNull 167 private List<byte[]> getOrCreateSegmentList(byte segmentType) 111 168 { 112 169 List<byte[]> segmentList; 113 if (_segmentDataMap.containsKey(segment Marker)) {114 segmentList = _segmentDataMap.get(segment Marker);170 if (_segmentDataMap.containsKey(segmentType)) { 171 segmentList = _segmentDataMap.get(segmentType); 115 172 } else { 116 173 segmentList = new ArrayList<byte[]>(); 117 _segmentDataMap.put(segment Marker, segmentList);174 _segmentDataMap.put(segmentType, segmentList); 118 175 } 119 176 return segmentList; … … 121 178 122 179 /** 123 * Returns the count of segment data byte arrays stored for a given segment marker. 124 * @param segmentMarker identifies the required segment 180 * Returns the count of segment data byte arrays stored for a given segment type. 181 * 182 * @param segmentType identifies the required segment 125 183 * @return the segment count (zero if no segments exist). 126 184 */ 127 public int getSegmentCount(byte segmentMarker) 128 { 129 final List<byte[]> segmentList = getSegmentList(segmentMarker); 185 public int getSegmentCount(@NotNull JpegSegmentType segmentType) 186 { 187 return getSegmentCount(segmentType.byteValue); 188 } 189 190 /** 191 * Returns the count of segment data byte arrays stored for a given segment type. 192 * 193 * @param segmentType identifies the required segment 194 * @return the segment count (zero if no segments exist). 195 */ 196 public int getSegmentCount(byte segmentType) 197 { 198 final List<byte[]> segmentList = getSegmentList(segmentType); 130 199 return segmentList == null ? 0 : segmentList.size(); 131 200 } … … 133 202 /** 134 203 * Removes a specified instance of a segment's data from the collection. Use this method when more than one 135 * occurrence of segment data for a given marker exists. 136 * @param segmentMarker identifies the required segment 137 * @param occurrence the zero-based index of the segment occurrence to remove. 138 */ 139 @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection" }) 140 public void removeSegmentOccurrence(byte segmentMarker, int occurrence) 141 { 142 final List<byte[]> segmentList = _segmentDataMap.get(Byte.valueOf(segmentMarker)); 204 * occurrence of segment data exists for a given type exists. 205 * 206 * @param segmentType identifies the required segment 207 * @param occurrence the zero-based index of the segment occurrence to remove. 208 */ 209 @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) 210 public void removeSegmentOccurrence(@NotNull JpegSegmentType segmentType, int occurrence) 211 { 212 removeSegmentOccurrence(segmentType.byteValue, occurrence); 213 } 214 215 /** 216 * Removes a specified instance of a segment's data from the collection. Use this method when more than one 217 * occurrence of segment data exists for a given type exists. 218 * 219 * @param segmentType identifies the required segment 220 * @param occurrence the zero-based index of the segment occurrence to remove. 221 */ 222 @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) 223 public void removeSegmentOccurrence(byte segmentType, int occurrence) 224 { 225 final List<byte[]> segmentList = _segmentDataMap.get(segmentType); 143 226 segmentList.remove(occurrence); 144 227 } 145 228 146 229 /** 147 * Removes all segments from the collection having the specified marker. 148 * @param segmentMarker identifies the required segment 149 */ 150 public void removeSegment(byte segmentMarker) 151 { 152 _segmentDataMap.remove(Byte.valueOf(segmentMarker)); 153 } 154 155 /** 156 * Determines whether data is present for a given segment marker. 157 * @param segmentMarker identifies the required segment 230 * Removes all segments from the collection having the specified type. 231 * 232 * @param segmentType identifies the required segment 233 */ 234 public void removeSegment(@NotNull JpegSegmentType segmentType) 235 { 236 removeSegment(segmentType.byteValue); 237 } 238 239 /** 240 * Removes all segments from the collection having the specified type. 241 * 242 * @param segmentType identifies the required segment 243 */ 244 public void removeSegment(byte segmentType) 245 { 246 _segmentDataMap.remove(segmentType); 247 } 248 249 /** 250 * Determines whether data is present for a given segment type. 251 * 252 * @param segmentType identifies the required segment 158 253 * @return true if data exists, otherwise false 159 254 */ 160 public boolean containsSegment(byte segmentMarker) 161 { 162 return _segmentDataMap.containsKey(Byte.valueOf(segmentMarker)); 163 } 164 165 /** 166 * Serialises the contents of a JpegSegmentData to a file. 167 * @param file to file to write from 168 * @param segmentData the data to write 169 * @throws IOException if problems occur while writing 170 */ 171 public static void toFile(@NotNull File file, @NotNull JpegSegmentData segmentData) throws IOException 172 { 173 FileOutputStream fileOutputStream = null; 174 try 175 { 176 fileOutputStream = new FileOutputStream(file); 177 new ObjectOutputStream(fileOutputStream).writeObject(segmentData); 178 } 179 finally 180 { 181 if (fileOutputStream!=null) 182 fileOutputStream.close(); 183 } 184 } 185 186 /** 187 * Deserialises the contents of a JpegSegmentData from a file. 188 * @param file the file to read from 189 * @return the JpegSegmentData as read 190 * @throws IOException if problems occur while reading 191 * @throws ClassNotFoundException if problems occur while deserialising 192 */ 193 @NotNull 194 public static JpegSegmentData fromFile(@NotNull File file) throws IOException, ClassNotFoundException 195 { 196 ObjectInputStream inputStream = null; 197 try 198 { 199 inputStream = new ObjectInputStream(new FileInputStream(file)); 200 return (JpegSegmentData)inputStream.readObject(); 201 } 202 finally 203 { 204 if (inputStream!=null) 205 inputStream.close(); 206 } 255 public boolean containsSegment(@NotNull JpegSegmentType segmentType) 256 { 257 return containsSegment(segmentType.byteValue); 258 } 259 260 /** 261 * Determines whether data is present for a given segment type. 262 * 263 * @param segmentType identifies the required segment 264 * @return true if data exists, otherwise false 265 */ 266 public boolean containsSegment(byte segmentType) 267 { 268 return _segmentDataMap.containsKey(segmentType); 207 269 } 208 270 } -
trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.imaging.jpeg; 22 22 23 import com.drew.lang.SequentialReader; 24 import com.drew.lang.StreamReader; 23 25 import com.drew.lang.annotations.NotNull; 24 26 import com.drew.lang.annotations.Nullable; 25 27 26 import java.io.*; 28 import java.io.File; 29 import java.io.FileInputStream; 30 import java.io.IOException; 31 import java.util.HashSet; 32 import java.util.Set; 27 33 28 34 /** 29 * Performs read functions of Jpeg files, returning specific file segments. 30 * @author Drew Noakes http://drewnoakes.com 35 * Performs read functions of JPEG files, returning specific file segments. 36 * <p> 37 * JPEG files are composed of a sequence of consecutive JPEG 'segments'. Each is identified by one of a set of byte 38 * values, modelled in the {@link JpegSegmentType} enumeration. Use <code>readSegments</code> to read out the some 39 * or all segments into a {@link JpegSegmentData} object, from which the raw JPEG segment byte arrays may be accessed. 40 * 41 * @author Drew Noakes https://drewnoakes.com 31 42 */ 32 43 public class JpegSegmentReader 33 44 { 34 // TODO add a findAvailableSegments() method35 // TODO add more segment identifiers36 // TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'37 38 @NotNull39 private final JpegSegmentData _segmentData;40 41 45 /** 42 46 * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet). 43 47 */ 44 private static final byte SEGMENT_SOS = (byte)0xDA; 48 private static final byte SEGMENT_SOS = (byte) 0xDA; 45 49 46 50 /** 47 51 * Private, because one wouldn't search for it. 48 52 */ 49 private static final byte MARKER_EOI = (byte)0xD9; 50 51 /** APP0 Jpeg segment identifier -- JFIF data (also JFXX apparently). */ 52 public static final byte SEGMENT_APP0 = (byte)0xE0; 53 /** APP1 Jpeg segment identifier -- where Exif data is kept. XMP data is also kept in here, though usually in a second instance. */ 54 public static final byte SEGMENT_APP1 = (byte)0xE1; 55 /** APP2 Jpeg segment identifier. */ 56 public static final byte SEGMENT_APP2 = (byte)0xE2; 57 /** APP3 Jpeg segment identifier. */ 58 public static final byte SEGMENT_APP3 = (byte)0xE3; 59 /** APP4 Jpeg segment identifier. */ 60 public static final byte SEGMENT_APP4 = (byte)0xE4; 61 /** APP5 Jpeg segment identifier. */ 62 public static final byte SEGMENT_APP5 = (byte)0xE5; 63 /** APP6 Jpeg segment identifier. */ 64 public static final byte SEGMENT_APP6 = (byte)0xE6; 65 /** APP7 Jpeg segment identifier. */ 66 public static final byte SEGMENT_APP7 = (byte)0xE7; 67 /** APP8 Jpeg segment identifier. */ 68 public static final byte SEGMENT_APP8 = (byte)0xE8; 69 /** APP9 Jpeg segment identifier. */ 70 public static final byte SEGMENT_APP9 = (byte)0xE9; 71 /** APPA (App10) Jpeg segment identifier -- can hold Unicode comments. */ 72 public static final byte SEGMENT_APPA = (byte)0xEA; 73 /** APPB (App11) Jpeg segment identifier. */ 74 public static final byte SEGMENT_APPB = (byte)0xEB; 75 /** APPC (App12) Jpeg segment identifier. */ 76 public static final byte SEGMENT_APPC = (byte)0xEC; 77 /** APPD (App13) Jpeg segment identifier -- IPTC data in here. */ 78 public static final byte SEGMENT_APPD = (byte)0xED; 79 /** APPE (App14) Jpeg segment identifier. */ 80 public static final byte SEGMENT_APPE = (byte)0xEE; 81 /** APPF (App15) Jpeg segment identifier. */ 82 public static final byte SEGMENT_APPF = (byte)0xEF; 83 /** Start Of Image segment identifier. */ 84 public static final byte SEGMENT_SOI = (byte)0xD8; 85 /** Define Quantization Table segment identifier. */ 86 public static final byte SEGMENT_DQT = (byte)0xDB; 87 /** Define Huffman Table segment identifier. */ 88 public static final byte SEGMENT_DHT = (byte)0xC4; 89 /** Start-of-Frame Zero segment identifier. */ 90 public static final byte SEGMENT_SOF0 = (byte)0xC0; 91 /** Jpeg comment segment identifier. */ 92 public static final byte SEGMENT_COM = (byte)0xFE; 53 private static final byte MARKER_EOI = (byte) 0xD9; 93 54 94 55 /** 95 * Creates a JpegSegmentReader for a specific file. 96 * @param file the Jpeg file to read segments from 56 * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object. 57 * <p> 58 * Will not return SOS (start of scan) or EOI (end of image) segments. 59 * 60 * @param file a {@link File} from which the JPEG data will be read. 61 * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code> 62 * then all found segment types are returned. 97 63 */ 98 @ SuppressWarnings({ "ConstantConditions" })99 public JpegSegment Reader(@NotNull File file) throws JpegProcessingException, IOException64 @NotNull 65 public static JpegSegmentData readSegments(@NotNull File file, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException 100 66 { 101 if (file==null) 102 throw new NullPointerException(); 103 104 InputStream inputStream = null; 67 FileInputStream stream = null; 105 68 try { 106 inputStream = new FileInputStream(file);107 _segmentData =readSegments(newBufferedInputStream(inputStream),false);69 stream = new FileInputStream(file); 70 return readSegments(new StreamReader(stream), segmentTypes); 108 71 } finally { 109 if (inputStream != null) 110 inputStream.close(); 72 if (stream != null) { 73 stream.close(); 74 } 111 75 } 112 76 } 113 77 114 78 /** 115 * Creates a JpegSegmentReader for a byte array. 116 * @param fileContents the byte array containing Jpeg data 79 * Processes the provided JPEG data, and extracts the specified JPEG segments into a {@link JpegSegmentData} object. 80 * <p> 81 * Will not return SOS (start of scan) or EOI (end of image) segments. 82 * 83 * @param reader a {@link SequentialReader} from which the JPEG data will be read. It must be positioned at the 84 * beginning of the JPEG data stream. 85 * @param segmentTypes the set of JPEG segments types that are to be returned. If this argument is <code>null</code> 86 * then all found segment types are returned. 117 87 */ 118 @ SuppressWarnings({ "ConstantConditions" })119 public JpegSegment Reader(@NotNullbyte[] fileContents) throws JpegProcessingException88 @NotNull 89 public static JpegSegmentData readSegments(@NotNull final SequentialReader reader, @Nullable Iterable<JpegSegmentType> segmentTypes) throws JpegProcessingException, IOException 120 90 { 121 if (fileContents==null)122 throw new NullPointerException();91 // Must be big-endian 92 assert (reader.isMotorolaByteOrder()); 123 93 124 BufferedInputStream stream = new BufferedInputStream(new ByteArrayInputStream(fileContents)); 125 _segmentData = readSegments(stream, false); 94 // first two bytes should be JPEG magic number 95 final int magicNumber = reader.getUInt16(); 96 if (magicNumber != 0xFFD8) { 97 throw new JpegProcessingException("JPEG data is expected to begin with 0xFFD8 (ÿØ) not 0x" + Integer.toHexString(magicNumber)); 98 } 99 100 Set<Byte> segmentTypeBytes = null; 101 if (segmentTypes != null) { 102 segmentTypeBytes = new HashSet<Byte>(); 103 for (JpegSegmentType segmentType : segmentTypes) { 104 segmentTypeBytes.add(segmentType.byteValue); 105 } 106 } 107 108 JpegSegmentData segmentData = new JpegSegmentData(); 109 110 do { 111 // Find the segment marker. Markers are zero or more 0xFF bytes, followed 112 // by a 0xFF and then a byte not equal to 0x00 or 0xFF. 113 114 final short segmentIdentifier = reader.getUInt8(); 115 116 // We must have at least one 0xFF byte 117 if (segmentIdentifier != 0xFF) 118 throw new JpegProcessingException("Expected JPEG segment start identifier 0xFF, not 0x" + Integer.toHexString(segmentIdentifier).toUpperCase()); 119 120 // Read until we have a non-0xFF byte. This identifies the segment type. 121 byte segmentType = reader.getInt8(); 122 while (segmentType == (byte)0xFF) 123 segmentType = reader.getInt8(); 124 125 if (segmentType == 0) 126 throw new JpegProcessingException("Expected non-zero byte as part of JPEG marker identifier"); 127 128 if (segmentType == SEGMENT_SOS) { 129 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would 130 // have to search for the two bytes: 0xFF 0xD9 (EOI). 131 // It comes last so simply return at this point 132 return segmentData; 133 } 134 135 if (segmentType == MARKER_EOI) { 136 // the 'End-Of-Image' segment -- this should never be found in this fashion 137 return segmentData; 138 } 139 140 // next 2-bytes are <segment-size>: [high-byte] [low-byte] 141 int segmentLength = reader.getUInt16(); 142 143 // segment length includes size bytes, so subtract two 144 segmentLength -= 2; 145 146 if (segmentLength < 0) 147 throw new JpegProcessingException("JPEG segment size would be less than zero"); 148 149 // Check whether we are interested in this segment 150 if (segmentTypeBytes == null || segmentTypeBytes.contains(segmentType)) { 151 byte[] segmentBytes = reader.getBytes(segmentLength); 152 assert (segmentLength == segmentBytes.length); 153 segmentData.addSegment(segmentType, segmentBytes); 154 } else { 155 // Some if the JPEG is truncated, just return what data we've already gathered 156 if (!reader.trySkip(segmentLength)) { 157 return segmentData; 158 } 159 } 160 161 } while (true); 126 162 } 127 163 128 /** 129 * Creates a JpegSegmentReader for an InputStream. 130 * @param inputStream the InputStream containing Jpeg data 131 */ 132 @SuppressWarnings({ "ConstantConditions" }) 133 public JpegSegmentReader(@NotNull InputStream inputStream, boolean waitForBytes) throws JpegProcessingException 164 private JpegSegmentReader() throws Exception 134 165 { 135 if (inputStream==null) 136 throw new NullPointerException(); 137 138 BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream 139 ? (BufferedInputStream)inputStream 140 : new BufferedInputStream(inputStream); 141 142 _segmentData = readSegments(bufferedInputStream, waitForBytes); 143 } 144 145 /** 146 * Reads the first instance of a given Jpeg segment, returning the contents as 147 * a byte array. 148 * @param segmentMarker the byte identifier for the desired segment 149 * @return the byte array if found, else null 150 */ 151 @Nullable 152 public byte[] readSegment(byte segmentMarker) 153 { 154 return readSegment(segmentMarker, 0); 155 } 156 157 /** 158 * Reads the Nth instance of a given Jpeg segment, returning the contents as a byte array. 159 * 160 * @param segmentMarker the byte identifier for the desired segment 161 * @param occurrence the occurrence of the specified segment within the jpeg file 162 * @return the byte array if found, else null 163 */ 164 @Nullable 165 public byte[] readSegment(byte segmentMarker, int occurrence) 166 { 167 return _segmentData.getSegment(segmentMarker, occurrence); 168 } 169 170 /** 171 * Returns all instances of a given Jpeg segment. If no instances exist, an empty sequence is returned. 172 * 173 * @param segmentMarker a number which identifies the type of Jpeg segment being queried 174 * @return zero or more byte arrays, each holding the data of a Jpeg segment 175 */ 176 @NotNull 177 public Iterable<byte[]> readSegments(byte segmentMarker) 178 { 179 return _segmentData.getSegments(segmentMarker); 180 } 181 182 /** 183 * Returns the number of segments having the specified JPEG segment marker. 184 * @param segmentMarker the JPEG segment identifying marker. 185 * @return the count of matching segments. 186 */ 187 public final int getSegmentCount(byte segmentMarker) 188 { 189 return _segmentData.getSegmentCount(segmentMarker); 190 } 191 192 /** 193 * Returns the JpegSegmentData object used by this reader. 194 * @return the JpegSegmentData object. 195 */ 196 @NotNull 197 public final JpegSegmentData getSegmentData() 198 { 199 return _segmentData; 200 } 201 202 @NotNull 203 private JpegSegmentData readSegments(@NotNull final BufferedInputStream jpegInputStream, boolean waitForBytes) throws JpegProcessingException 204 { 205 JpegSegmentData segmentData = new JpegSegmentData(); 206 207 try { 208 int offset = 0; 209 // first two bytes should be jpeg magic number 210 byte[] headerBytes = new byte[2]; 211 if (jpegInputStream.read(headerBytes, 0, 2)!=2) 212 throw new JpegProcessingException("not a jpeg file"); 213 final boolean hasValidHeader = (headerBytes[0] & 0xFF) == 0xFF && (headerBytes[1] & 0xFF) == 0xD8; 214 if (!hasValidHeader) 215 throw new JpegProcessingException("not a jpeg file"); 216 217 offset += 2; 218 do { 219 // need four bytes from stream for segment header before continuing 220 if (!checkForBytesOnStream(jpegInputStream, 4, waitForBytes)) 221 throw new JpegProcessingException("stream ended before segment header could be read"); 222 223 // next byte is 0xFF 224 byte segmentIdentifier = (byte)(jpegInputStream.read() & 0xFF); 225 if ((segmentIdentifier & 0xFF) != 0xFF) { 226 throw new JpegProcessingException("expected jpeg segment start identifier 0xFF at offset " + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF)); 227 } 228 offset++; 229 // next byte is <segment-marker> 230 byte thisSegmentMarker = (byte)(jpegInputStream.read() & 0xFF); 231 offset++; 232 // next 2-bytes are <segment-size>: [high-byte] [low-byte] 233 byte[] segmentLengthBytes = new byte[2]; 234 if (jpegInputStream.read(segmentLengthBytes, 0, 2) != 2) 235 throw new JpegProcessingException("Jpeg data ended unexpectedly."); 236 offset += 2; 237 int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF); 238 // segment length includes size bytes, so subtract two 239 segmentLength -= 2; 240 if (!checkForBytesOnStream(jpegInputStream, segmentLength, waitForBytes)) 241 throw new JpegProcessingException("segment size would extend beyond file stream length"); 242 if (segmentLength < 0) 243 throw new JpegProcessingException("segment size would be less than zero"); 244 byte[] segmentBytes = new byte[segmentLength]; 245 if (jpegInputStream.read(segmentBytes, 0, segmentLength) != segmentLength) 246 throw new JpegProcessingException("Jpeg data ended unexpectedly."); 247 offset += segmentLength; 248 if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) { 249 // The 'Start-Of-Scan' segment's length doesn't include the image data, instead would 250 // have to search for the two bytes: 0xFF 0xD9 (EOI). 251 // It comes last so simply return at this point 252 return segmentData; 253 } else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) { 254 // the 'End-Of-Image' segment -- this should never be found in this fashion 255 return segmentData; 256 } else { 257 segmentData.addSegment(thisSegmentMarker, segmentBytes); 258 } 259 } while (true); 260 } catch (IOException ioe) { 261 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe); 262 } finally { 263 try { 264 if (jpegInputStream != null) { 265 jpegInputStream.close(); 266 } 267 } catch (IOException ioe) { 268 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe); 269 } 270 } 271 } 272 273 private boolean checkForBytesOnStream(@NotNull BufferedInputStream stream, int bytesNeeded, boolean waitForBytes) throws IOException 274 { 275 // NOTE waiting is essential for network streams where data can be delayed, but it is not necessary for byte[] or filesystems 276 277 if (!waitForBytes) 278 return bytesNeeded <= stream.available(); 279 280 int count = 40; // * 100ms = approx 4 seconds 281 while (count > 0) { 282 if (bytesNeeded <= stream.available()) 283 return true; 284 try { 285 Thread.sleep(100); 286 } catch (InterruptedException e) { 287 // continue 288 } 289 count--; 290 } 291 return false; 166 throw new Exception("Not intended for instantiation."); 292 167 } 293 168 } -
trunk/src/com/drew/lang/BufferBoundsException.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 22 22 package com.drew.lang; 23 23 24 import com.drew.lang.annotations.NotNull;25 26 24 import java.io.IOException; 27 25 28 26 /** 29 * A checked replacement for IndexOutOfBoundsException. Used by BufferReader.30 * 31 * @author Drew Noakes http://drewnoakes.com 27 * A checked replacement for {@link IndexOutOfBoundsException}. Used by {@link RandomAccessReader}. 28 * 29 * @author Drew Noakes https://drewnoakes.com 32 30 */ 33 public final class BufferBoundsException extends Exception 31 public final class BufferBoundsException extends IOException 34 32 { 35 33 private static final long serialVersionUID = 2911102837808946396L; 36 34 37 public BufferBoundsException( @NotNull byte[] buffer,int index, int bytesRequested)35 public BufferBoundsException(int index, int bytesRequested, long bufferLength) 38 36 { 39 super(getMessage( buffer,index, bytesRequested));37 super(getMessage(index, bytesRequested, bufferLength)); 40 38 } 41 39 … … 45 43 } 46 44 47 public BufferBoundsException(final String message, final IOException innerException) 48 { 49 super(message, innerException); 50 } 51 52 private static String getMessage(@NotNull byte[] buffer, int index, int bytesRequested) 45 private static String getMessage(int index, int bytesRequested, long bufferLength) 53 46 { 54 47 if (index < 0) 55 return String.format("Attempt to read from buffer using a negative index (% s)", index);48 return String.format("Attempt to read from buffer using a negative index (%d)", index); 56 49 57 return String.format("Attempt to read %d byte%s from beyond end of buffer (requested index: %d, max index: %d)", 58 bytesRequested, bytesRequested==1?"":"s", index, buffer.length - 1); 50 if (bytesRequested < 0) 51 return String.format("Number of requested bytes cannot be negative (%d)", bytesRequested); 52 53 if ((long)index + (long)bytesRequested - 1L > (long)Integer.MAX_VALUE) 54 return String.format("Number of requested bytes summed with starting index exceed maximum range of signed 32 bit integers (requested index: %d, requested count: %d)", index, bytesRequested); 55 56 return String.format("Attempt to read from beyond end of underlying data source (requested index: %d, requested count: %d, max index: %d)", 57 index, bytesRequested, bufferLength - 1); 59 58 } 60 59 } -
trunk/src/com/drew/lang/ByteArrayReader.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 24 24 import com.drew.lang.annotations.NotNull; 25 25 26 import java.io. UnsupportedEncodingException;26 import java.io.IOException; 27 27 28 28 /** 29 29 * Provides methods to read specific values from a byte array, with a consistent, checked exception structure for 30 30 * issues. 31 * <p />31 * <p> 32 32 * By default, the reader operates with Motorola byte order (big endianness). This can be changed by calling 33 * {@seesetMotorolaByteOrder(boolean)}.34 * 35 * @author Drew Noakes http://drewnoakes.com 33 * <code>setMotorolaByteOrder(boolean)</code>. 34 * 35 * @author Drew Noakes https://drewnoakes.com 36 36 * */ 37 public class ByteArrayReader implements BufferReader37 public class ByteArrayReader extends RandomAccessReader 38 38 { 39 39 @NotNull 40 40 private final byte[] _buffer; 41 private boolean _isMotorolaByteOrder = true;42 41 43 42 @SuppressWarnings({ "ConstantConditions" }) … … 47 46 if (buffer == null) 48 47 throw new NullPointerException(); 49 48 50 49 _buffer = buffer; 51 50 } … … 57 56 } 58 57 59 60 58 @Override 61 p ublic void setMotorolaByteOrder(boolean motorolaByteOrder)59 protected byte getByte(int index) throws IOException 62 60 { 63 _isMotorolaByteOrder = motorolaByteOrder;64 }65 66 @Override67 public boolean isMotorolaByteOrder()68 {69 return _isMotorolaByteOrder;70 }71 72 @Override73 public short getUInt8(int index) throws BufferBoundsException74 {75 checkBounds(index, 1);76 77 return (short) (_buffer[index] & 255);78 }79 80 @Override81 public byte getInt8(int index) throws BufferBoundsException82 {83 checkBounds(index, 1);84 85 61 return _buffer[index]; 86 62 } 87 63 88 64 @Override 89 p ublic int getUInt16(int index) throwsBufferBoundsException65 protected void validateIndex(int index, int bytesRequested) throws IOException 90 66 { 91 checkBounds(index, 2); 92 93 if (_isMotorolaByteOrder) { 94 // Motorola - MSB first 95 return (_buffer[index ] << 8 & 0xFF00) | 96 (_buffer[index + 1] & 0xFF); 97 } else { 98 // Intel ordering - LSB first 99 return (_buffer[index + 1] << 8 & 0xFF00) | 100 (_buffer[index ] & 0xFF); 101 } 67 if (!isValidIndex(index, bytesRequested)) 68 throw new BufferBoundsException(index, bytesRequested, _buffer.length); 102 69 } 103 70 104 71 @Override 105 p ublic short getInt16(int index) throwsBufferBoundsException72 protected boolean isValidIndex(int index, int bytesRequested) throws IOException 106 73 { 107 checkBounds(index, 2); 108 109 if (_isMotorolaByteOrder) { 110 // Motorola - MSB first 111 return (short) (((short)_buffer[index ] << 8 & (short)0xFF00) | 112 ((short)_buffer[index + 1] & (short)0xFF)); 113 } else { 114 // Intel ordering - LSB first 115 return (short) (((short)_buffer[index + 1] << 8 & (short)0xFF00) | 116 ((short)_buffer[index ] & (short)0xFF)); 117 } 74 return bytesRequested >= 0 75 && index >= 0 76 && (long)index + (long)bytesRequested - 1L < (long)_buffer.length; 118 77 } 119 78 120 79 @Override 121 public long getUInt32(int index) throws BufferBoundsException 80 @NotNull 81 public byte[] getBytes(int index, int count) throws IOException 122 82 { 123 checkBounds(index, 4); 124 125 if (_isMotorolaByteOrder) { 126 // Motorola - MSB first (big endian) 127 return (((long)_buffer[index ]) << 24 & 0xFF000000L) | 128 (((long)_buffer[index + 1]) << 16 & 0xFF0000L) | 129 (((long)_buffer[index + 2]) << 8 & 0xFF00L) | 130 (((long)_buffer[index + 3]) & 0xFFL); 131 } else { 132 // Intel ordering - LSB first (little endian) 133 return (((long)_buffer[index + 3]) << 24 & 0xFF000000L) | 134 (((long)_buffer[index + 2]) << 16 & 0xFF0000L) | 135 (((long)_buffer[index + 1]) << 8 & 0xFF00L) | 136 (((long)_buffer[index ]) & 0xFFL); 137 } 138 } 139 140 @Override 141 public int getInt32(int index) throws BufferBoundsException 142 { 143 checkBounds(index, 4); 144 145 if (_isMotorolaByteOrder) { 146 // Motorola - MSB first (big endian) 147 return (_buffer[index ] << 24 & 0xFF000000) | 148 (_buffer[index + 1] << 16 & 0xFF0000) | 149 (_buffer[index + 2] << 8 & 0xFF00) | 150 (_buffer[index + 3] & 0xFF); 151 } else { 152 // Intel ordering - LSB first (little endian) 153 return (_buffer[index + 3] << 24 & 0xFF000000) | 154 (_buffer[index + 2] << 16 & 0xFF0000) | 155 (_buffer[index + 1] << 8 & 0xFF00) | 156 (_buffer[index ] & 0xFF); 157 } 158 } 159 160 @Override 161 public long getInt64(int index) throws BufferBoundsException 162 { 163 checkBounds(index, 8); 164 165 if (_isMotorolaByteOrder) { 166 // Motorola - MSB first 167 return ((long)_buffer[index ] << 56 & 0xFF00000000000000L) | 168 ((long)_buffer[index + 1] << 48 & 0xFF000000000000L) | 169 ((long)_buffer[index + 2] << 40 & 0xFF0000000000L) | 170 ((long)_buffer[index + 3] << 32 & 0xFF00000000L) | 171 ((long)_buffer[index + 4] << 24 & 0xFF000000L) | 172 ((long)_buffer[index + 5] << 16 & 0xFF0000L) | 173 ((long)_buffer[index + 6] << 8 & 0xFF00L) | 174 ((long)_buffer[index + 7] & 0xFFL); 175 } else { 176 // Intel ordering - LSB first 177 return ((long)_buffer[index + 7] << 56 & 0xFF00000000000000L) | 178 ((long)_buffer[index + 6] << 48 & 0xFF000000000000L) | 179 ((long)_buffer[index + 5] << 40 & 0xFF0000000000L) | 180 ((long)_buffer[index + 4] << 32 & 0xFF00000000L) | 181 ((long)_buffer[index + 3] << 24 & 0xFF000000L) | 182 ((long)_buffer[index + 2] << 16 & 0xFF0000L) | 183 ((long)_buffer[index + 1] << 8 & 0xFF00L) | 184 ((long)_buffer[index ] & 0xFFL); 185 } 186 } 187 188 @Override 189 public float getS15Fixed16(int index) throws BufferBoundsException 190 { 191 checkBounds(index, 4); 192 193 if (_isMotorolaByteOrder) { 194 float res = (_buffer[index ] & 255) << 8 | 195 (_buffer[index + 1] & 255); 196 int d = (_buffer[index + 2] & 255) << 8 | 197 (_buffer[index + 3] & 255); 198 return (float)(res + d/65536.0); 199 } else { 200 // this particular branch is untested 201 float res = (_buffer[index + 3] & 255) << 8 | 202 (_buffer[index + 2] & 255); 203 int d = (_buffer[index + 1] & 255) << 8 | 204 (_buffer[index ] & 255); 205 return (float)(res + d/65536.0); 206 } 207 } 208 209 @Override 210 public float getFloat32(int index) throws BufferBoundsException 211 { 212 return Float.intBitsToFloat(getInt32(index)); 213 } 214 215 @Override 216 public double getDouble64(int index) throws BufferBoundsException 217 { 218 return Double.longBitsToDouble(getInt64(index)); 219 } 220 221 @Override 222 @NotNull 223 public byte[] getBytes(int index, int count) throws BufferBoundsException 224 { 225 checkBounds(index, count); 83 validateIndex(index, count); 226 84 227 85 byte[] bytes = new byte[count]; … … 229 87 return bytes; 230 88 } 231 232 @Override233 @NotNull234 public String getString(int index, int bytesRequested) throws BufferBoundsException235 {236 return new String(getBytes(index, bytesRequested));237 }238 239 @Override240 @NotNull241 public String getString(int index, int bytesRequested, String charset) throws BufferBoundsException242 {243 byte[] bytes = getBytes(index, bytesRequested);244 try {245 return new String(bytes, charset);246 } catch (UnsupportedEncodingException e) {247 return new String(bytes);248 }249 }250 251 @Override252 @NotNull253 public String getNullTerminatedString(int index, int maxLengthBytes) throws BufferBoundsException254 {255 // NOTE currently only really suited to single-byte character strings256 257 checkBounds(index, maxLengthBytes);258 259 // Check for null terminators260 int length = 0;261 while ((index + length) < _buffer.length && _buffer[index + length] != '\0' && length < maxLengthBytes)262 length++;263 264 byte[] bytes = getBytes(index, length);265 return new String(bytes);266 }267 268 private void checkBounds(final int index, final int bytesRequested) throws BufferBoundsException269 {270 if (bytesRequested < 0 || index < 0 || (long)index + (long)bytesRequested - 1L >= (long)_buffer.length)271 throw new BufferBoundsException(_buffer, index, bytesRequested);272 }273 89 } -
trunk/src/com/drew/lang/CompoundException.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.lang; … … 32 32 * of these previous JDK versions. 33 33 * 34 * @author Drew Noakes http://drewnoakes.com 34 * @author Drew Noakes https://drewnoakes.com 35 35 */ 36 36 public class CompoundException extends Exception … … 63 63 } 64 64 65 @Override 65 66 @NotNull 66 67 public String toString() … … 77 78 } 78 79 80 @Override 79 81 public void printStackTrace(@NotNull PrintStream s) 80 82 { … … 86 88 } 87 89 90 @Override 88 91 public void printStackTrace(@NotNull PrintWriter s) 89 92 { … … 95 98 } 96 99 100 @Override 97 101 public void printStackTrace() 98 102 { -
trunk/src/com/drew/lang/GeoLocation.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 25 25 import com.drew.lang.annotations.Nullable; 26 26 27 import java.text.DecimalFormat; 28 27 29 /** 28 30 * Represents a latitude and longitude pair, giving a position on earth in spherical coordinates. 31 * <p> 29 32 * Values of latitude and longitude are given in degrees. 33 * <p> 30 34 * This type is immutable. 31 35 */ … … 79 83 { 80 84 double[] dms = decimalToDegreesMinutesSeconds(decimal); 81 return dms[0] + "° " + dms[1] + "' " + dms[2] + '"'; 85 DecimalFormat format = new DecimalFormat("0.##"); 86 return String.format("%s° %s' %s\"", format.format(dms[0]), format.format(dms[1]), format.format(dms[2])); 82 87 } 83 88 -
trunk/src/com/drew/lang/NullOutputStream.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.lang; … … 27 27 * An implementation of OutputSteam that ignores write requests by doing nothing. This class may be useful in tests. 28 28 * 29 * @author Drew Noakes http://drewnoakes.com 29 * @author Drew Noakes https://drewnoakes.com 30 30 */ 31 31 public class NullOutputStream extends OutputStream … … 36 36 } 37 37 38 @Override 38 39 public void write(int b) throws IOException 39 40 { -
trunk/src/com/drew/lang/Rational.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 29 29 /** 30 30 * Immutable class for holding a rational number without loss of precision. Provides 31 * a familiar representation via toString() in form <code>numerator/denominator</code>. 32 * 33 * @author Drew Noakes http://drewnoakes.com 31 * a familiar representation via {@link Rational#toString} in form <code>numerator/denominator</code>. 32 * 33 * Note that any value with a numerator of zero will be treated as zero, even if the 34 * denominator is also zero. 35 * 36 * @author Drew Noakes https://drewnoakes.com 34 37 */ 35 38 public class Rational extends java.lang.Number implements Serializable … … 61 64 * to type <code>double</code>. 62 65 */ 66 @Override 63 67 public double doubleValue() 64 68 { 65 return (double) _numerator / (double) _denominator; 69 return _numerator == 0 70 ? 0.0 71 : (double) _numerator / (double) _denominator; 66 72 } 67 73 … … 73 79 * to type <code>float</code>. 74 80 */ 81 @Override 75 82 public float floatValue() 76 83 { 77 return (float) _numerator / (float) _denominator; 84 return _numerator == 0 85 ? 0.0f 86 : (float) _numerator / (float) _denominator; 78 87 } 79 88 … … 81 90 * Returns the value of the specified number as a <code>byte</code>. 82 91 * This may involve rounding or truncation. This implementation simply 83 * casts the result of <code>doubleValue()</code>to <code>byte</code>.92 * casts the result of {@link Rational#doubleValue} to <code>byte</code>. 84 93 * 85 94 * @return the numeric value represented by this object after conversion 86 95 * to type <code>byte</code>. 87 96 */ 97 @Override 88 98 public final byte byteValue() 89 99 { … … 94 104 * Returns the value of the specified number as an <code>int</code>. 95 105 * This may involve rounding or truncation. This implementation simply 96 * casts the result of <code>doubleValue()</code>to <code>int</code>.106 * casts the result of {@link Rational#doubleValue} to <code>int</code>. 97 107 * 98 108 * @return the numeric value represented by this object after conversion 99 109 * to type <code>int</code>. 100 110 */ 111 @Override 101 112 public final int intValue() 102 113 { … … 107 118 * Returns the value of the specified number as a <code>long</code>. 108 119 * This may involve rounding or truncation. This implementation simply 109 * casts the result of <code>doubleValue()</code>to <code>long</code>.120 * casts the result of {@link Rational#doubleValue} to <code>long</code>. 110 121 * 111 122 * @return the numeric value represented by this object after conversion 112 123 * to type <code>long</code>. 113 124 */ 125 @Override 114 126 public final long longValue() 115 127 { … … 120 132 * Returns the value of the specified number as a <code>short</code>. 121 133 * This may involve rounding or truncation. This implementation simply 122 * casts the result of <code>doubleValue()</code>to <code>short</code>.134 * casts the result of {@link Rational#doubleValue} to <code>short</code>. 123 135 * 124 136 * @return the numeric value represented by this object after conversion 125 137 * to type <code>short</code>. 126 138 */ 139 @Override 127 140 public final short shortValue() 128 141 { … … 154 167 } 155 168 156 /** Checks if this rationalnumber is an Integer, either positive or negative. */169 /** Checks if this {@link Rational} number is an Integer, either positive or negative. */ 157 170 public boolean isInteger() 158 171 { … … 167 180 * @return a string representation of the object. 168 181 */ 182 @Override 169 183 @NotNull 170 184 public String toString() … … 173 187 } 174 188 175 /** Returns the simplest representation of this Rational's value possible. */189 /** Returns the simplest representation of this {@link Rational}'s value possible. */ 176 190 @NotNull 177 191 public String toSimpleString(boolean allowDecimal) … … 211 225 212 226 /** 213 * Compares two <code>Rational</code>instances, returning true if they are mathematically227 * Compares two {@link Rational} instances, returning true if they are mathematically 214 228 * equivalent. 215 229 * 216 * @param obj the Rationalto compare this instance to.230 * @param obj the {@link Rational} to compare this instance to. 217 231 * @return true if instances are mathematically equivalent, otherwise false. Will also 218 * return false if <code>obj</code> is not an instance of <code>Rational</code>.232 * return false if <code>obj</code> is not an instance of {@link Rational}. 219 233 */ 220 234 @Override … … 235 249 /** 236 250 * <p> 237 * Simplifies the Rationalnumber.</p>251 * Simplifies the {@link Rational} number.</p> 238 252 * <p> 239 253 * Prime number series: 1, 2, 3, 5, 7, 9, 11, 13, 17</p> … … 244 258 * <p> 245 259 * However, generating the prime number series seems to be a hefty task. Perhaps 246 * it's simpler to check if both d & n are divisible by all numbers from 2 ->260 * it's simpler to check if both d & n are divisible by all numbers from 2 {@literal ->} 247 261 * (Math.min(denominator, numerator) / 2). In doing this, one can check for 2 248 262 * and 5 once, then ignore all even numbers, and all numbers ending in 0 or 5. … … 250 264 * <p> 251 265 * Therefore, the max number of pairs of modulus divisions required will be:</p> 252 * < code><pre>266 * <pre><code> 253 267 * 4 Math.min(denominator, numerator) - 1 254 268 * -- * ------------------------------------ + 2 255 269 * 10 2 256 * <p/>270 * 257 271 * Math.min(denominator, numerator) - 1 258 272 * = ------------------------------------ + 2 259 273 * 5 260 * </ pre></code>274 * </code></pre> 261 275 * 262 276 * @return a simplified instance, or if the Rational could not be simplified, -
trunk/src/com/drew/lang/StringUtil.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 23 23 24 24 import com.drew.lang.annotations.NotNull; 25 import com.drew.lang.annotations.Nullable; 25 26 27 import java.io.BufferedReader; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.InputStreamReader; 26 31 import java.util.Iterator; 27 32 28 /** @author Drew Noakes http://drewnoakes.com */ 33 /** 34 * @author Drew Noakes https://drewnoakes.com 35 */ 29 36 public class StringUtil 30 37 { 38 @NotNull 31 39 public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter) 32 40 { … … 50 58 } 51 59 60 @NotNull 52 61 public static <T extends CharSequence> String join(@NotNull T[] strings, @NotNull String delimiter) 53 62 { … … 69 78 return buffer.toString(); 70 79 } 80 81 @NotNull 82 public static String fromStream(@NotNull InputStream stream) throws IOException 83 { 84 BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); 85 StringBuilder sb = new StringBuilder(); 86 String line; 87 while ((line = reader.readLine()) != null) { 88 sb.append(line); 89 } 90 return sb.toString(); 91 } 92 93 public static int compare(@Nullable String s1, @Nullable String s2) 94 { 95 boolean null1 = s1 == null; 96 boolean null2 = s2 == null; 97 98 if (null1 && null2) { 99 return 0; 100 } else if (null1) { 101 return -1; 102 } else if (null2) { 103 return 1; 104 } else { 105 return s1.compareTo(s2); 106 } 107 } 108 109 @NotNull 110 public static String urlEncode(@NotNull String name) 111 { 112 // Sufficient for now, it seems 113 return name.replace(" ", "%20"); 114 } 71 115 } -
trunk/src/com/drew/lang/annotations/NotNull.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 23 23 24 24 /** 25 * @author Drew Noakes http://drewnoakes.com 25 * @author Drew Noakes https://drewnoakes.com 26 26 */ 27 27 public @interface NotNull -
trunk/src/com/drew/lang/annotations/Nullable.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 23 23 24 24 /** 25 * @author Drew Noakes http://drewnoakes.com 25 * @author Drew Noakes https://drewnoakes.com 26 26 */ 27 27 public @interface Nullable -
trunk/src/com/drew/lang/annotations/SuppressWarnings.java
r6127 r8132 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 -
trunk/src/com/drew/metadata/Age.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 27 27 /** 28 28 * Represents an age in years, months, days, hours, minutes and seconds. 29 * <p />29 * <p> 30 30 * Used by certain Panasonic cameras which have face recognition features. 31 31 * 32 * @author Drew Noakes http://drewnoakes.com 32 * @author Drew Noakes https://drewnoakes.com 33 33 */ 34 34 public class Age 35 35 { 36 private int _years; 37 private int _months; 38 private int _days; 39 private int _hours; 40 private int _minutes; 41 private int _seconds; 36 private final int _years; 37 private final int _months; 38 private final int _days; 39 private final int _hours; 40 private final int _minutes; 41 private final int _seconds; 42 42 43 43 /** 44 44 * Parses an age object from the string format used by Panasonic cameras: 45 45 * <code>0031:07:15 00:00:00</code> 46 * 46 47 * @param s The String in format <code>0031:07:15 00:00:00</code>. 47 48 * @return The parsed Age object, or null if the value could not be parsed -
trunk/src/com/drew/metadata/DefaultTagDescriptor.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; … … 28 28 * and gives descriptions using the default string representation of the value. 29 29 * 30 * @author Drew Noakes http://drewnoakes.com 30 * @author Drew Noakes https://drewnoakes.com 31 31 */ 32 32 public class DefaultTagDescriptor extends TagDescriptor<Directory> -
trunk/src/com/drew/metadata/Directory.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; … … 37 37 * data types. 38 38 * 39 * @author Drew Noakes http://drewnoakes.com 39 * @author Drew Noakes https://drewnoakes.com 40 40 */ 41 41 public abstract class Directory 42 42 { 43 // TODO get Array methods need to return cloned data, to maintain this directory's integrity44 45 43 /** Map of values hashed by type identifiers. */ 46 44 @NotNull … … 104 102 public Collection<Tag> getTags() 105 103 { 106 return _definedTagList; 104 return Collections.unmodifiableCollection(_definedTagList); 107 105 } 108 106 … … 158 156 public Iterable<String> getErrors() 159 157 { 160 return _errorList; 158 return Collections.unmodifiableCollection(_errorList); 161 159 } 162 160 … … 414 412 return null; 415 413 416 if (o instanceof String) { 414 if (o instanceof Number) { 415 return ((Number)o).intValue(); 416 } else if (o instanceof String) { 417 417 try { 418 418 return Integer.parseInt((String)o); … … 428 428 return (int)val; 429 429 } 430 } else if (o instanceof Number) {431 return ((Number)o).intValue();432 430 } else if (o instanceof Rational[]) { 433 431 Rational[] rationals = (Rational[])o; … … 498 496 if (o == null) 499 497 return null; 498 if (o instanceof int[]) 499 return (int[])o; 500 500 if (o instanceof Rational[]) { 501 501 Rational[] rationals = (Rational[])o; … … 506 506 return ints; 507 507 } 508 if (o instanceof int[]) 509 return (int[])o; 508 if (o instanceof short[]) { 509 short[] shorts = (short[])o; 510 int[] ints = new int[shorts.length]; 511 for (int i = 0; i < shorts.length; i++) { 512 ints[i] = shorts[i]; 513 } 514 return ints; 515 } 510 516 if (o instanceof byte[]) { 511 517 byte[] bytes = (byte[])o; 512 518 int[] ints = new int[bytes.length]; 513 519 for (int i = 0; i < bytes.length; i++) { 514 byte b = bytes[i]; 515 ints[i] = b; 520 ints[i] = bytes[i]; 516 521 } 517 522 return ints; … … 527 532 if (o instanceof Integer) 528 533 return new int[] { (Integer)o }; 529 534 530 535 return null; 531 536 } … … 560 565 } 561 566 return bytes; 567 } else if (o instanceof short[]) { 568 short[] shorts = (short[])o; 569 byte[] bytes = new byte[shorts.length]; 570 for (int i = 0; i < shorts.length; i++) { 571 bytes[i] = (byte)shorts[i]; 572 } 573 return bytes; 562 574 } else if (o instanceof CharSequence) { 563 575 CharSequence str = (CharSequence)o; … … 703 715 /** 704 716 * Returns the specified tag's value as a java.util.Date. If the value is unset or cannot be converted, <code>null</code> is returned. 705 * <p />717 * <p> 706 718 * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in 707 719 * the current {@link TimeZone}. If the {@link TimeZone} is known, call the overload that accepts one as an argument. … … 712 724 return getDate(tagType, null); 713 725 } 714 726 715 727 /** 716 728 * Returns the specified tag's value as a java.util.Date. If the value is unset or cannot be converted, <code>null</code> is returned. 717 * <p />729 * <p> 718 730 * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in 719 731 * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null). Note that this parameter … … 818 830 boolean isLongArray = componentType.getName().equals("long"); 819 831 boolean isByteArray = componentType.getName().equals("byte"); 832 boolean isShortArray = componentType.getName().equals("short"); 820 833 StringBuilder string = new StringBuilder(); 821 834 for (int i = 0; i < arrayLength; i++) { … … 826 839 else if (isIntArray) 827 840 string.append(Array.getInt(o, i)); 841 else if (isShortArray) 842 string.append(Array.getShort(o, i)); 828 843 else if (isLongArray) 829 844 string.append(Array.getLong(o, i)); … … 896 911 897 912 /** 913 * Gets whether the specified tag is known by the directory and has a name. 914 * 915 * @param tagType the tag type identifier 916 * @return whether this directory has a name for the specified tag 917 */ 918 public boolean hasTagName(int tagType) 919 { 920 return getTagNameMap().containsKey(tagType); 921 } 922 923 /** 898 924 * Provides a description of a tag's value using the descriptor set by 899 925 * <code>setDescriptor(Descriptor)</code>. … … 908 934 return _descriptor.getDescription(tagType); 909 935 } 936 937 @Override 938 public String toString() 939 { 940 return String.format("%s Directory (%d %s)", 941 getName(), 942 _tagMap.size(), 943 _tagMap.size() == 1 944 ? "tag" 945 : "tags"); 946 } 910 947 } -
trunk/src/com/drew/metadata/Face.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; … … 26 26 /** 27 27 * Class to hold information about a detected or recognized face in a photo. 28 * <p />28 * <p> 29 29 * When a face is <em>detected</em>, the camera believes that a face is present at a given location in 30 30 * the image, but is not sure whose face it is. When a face is <em>recognised</em>, then the face is … … 116 116 } 117 117 118 @Override 118 119 @NotNull 119 120 public String toString() -
trunk/src/com/drew/metadata/Metadata.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; … … 24 24 import com.drew.lang.annotations.Nullable; 25 25 26 import java.util.ArrayList; 27 import java.util.Collection; 28 import java.util.HashMap; 29 import java.util.Map; 26 import java.util.*; 30 27 31 28 /** 32 * A top-level object to hold the various types of metadata (Exif/IPTC/etc) related to one entity (such as a file 33 * or stream). 34 * <p/> 35 * Metadata objects may contain zero or more directories. Each directory may contain zero or more tags with 36 * corresponding values. 29 * A top-level object that holds the metadata values extracted from an image. 30 * <p> 31 * Metadata objects may contain zero or more {@link Directory} objects. Each directory may contain zero or more tags 32 * with corresponding values. 37 33 * 38 * @author Drew Noakes http://drewnoakes.com 34 * @author Drew Noakes https://drewnoakes.com 39 35 */ 40 36 public final class Metadata … … 42 38 @NotNull 43 39 private final Map<Class<? extends Directory>,Directory> _directoryByClass = new HashMap<Class<? extends Directory>, Directory>(); 44 40 45 41 /** 46 42 * List of Directory objects set against this object. Keeping a list handy makes … … 58 54 public Iterable<Directory> getDirectories() 59 55 { 60 return _directoryList; 56 return Collections.unmodifiableCollection(_directoryList); 61 57 } 62 58 … … 72 68 73 69 /** 74 * Returns a <code>Directory</code>of specified type. If this<code>Metadata</code>object already contains70 * Returns a {@link Directory} of specified type. If this {@link Metadata} object already contains 75 71 * such a directory, it is returned. Otherwise a new instance of this directory will be created and stored within 76 * this Metadataobject.72 * this {@link Metadata} object. 77 73 * 78 74 * @param type the type of the Directory implementation required. … … 104 100 105 101 /** 106 * If this <code>Metadata</code>object contains a<code>Directory</code>of the specified type, it is returned.102 * If this {@link Metadata} object contains a {@link Directory} of the specified type, it is returned. 107 103 * Otherwise <code>null</code> is returned. 108 104 * 109 105 * @param type the Directory type 110 106 * @param <T> the Directory type 111 * @return a Directory of type T if it exists in this Metadataobject, otherwise <code>null</code>.107 * @return a Directory of type T if it exists in this {@link Metadata} object, otherwise <code>null</code>. 112 108 */ 113 109 @Nullable … … 123 119 /** 124 120 * Indicates whether a given directory type has been created in this metadata 125 * repository. Directories are created by calling <code>getOrCreateDirectory(Class)</code>.121 * repository. Directories are created by calling {@link Metadata#getOrCreateDirectory(Class)}. 126 122 * 127 * @param type the Directory type 128 * @return true if the metadata directory has been created123 * @param type the {@link Directory} type 124 * @return true if the {@link Directory} has been created 129 125 */ 130 126 public boolean containsDirectory(Class<? extends Directory> type) … … 135 131 /** 136 132 * Indicates whether any errors were reported during the reading of metadata values. 137 * This value will be true if Directory.hasErrors() is true for one of the contained Directory objects. 133 * This value will be true if Directory.hasErrors() is true for one of the contained {@link Directory} objects. 138 134 * 139 135 * @return whether one of the contained directories has an error … … 147 143 return false; 148 144 } 145 146 @Override 147 public String toString() 148 { 149 return String.format("Metadata (%d %s)", 150 _directoryList.size(), 151 _directoryList.size() == 1 152 ? "directory" 153 : "directories"); 154 } 149 155 } -
trunk/src/com/drew/metadata/MetadataException.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; … … 27 27 * Base class for all metadata specific exceptions. 28 28 * 29 * @author Drew Noakes http://drewnoakes.com 29 * @author Drew Noakes https://drewnoakes.com 30 30 */ 31 31 public class MetadataException extends CompoundException -
trunk/src/com/drew/metadata/MetadataReader.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; 22 22 23 import com.drew.lang. BufferReader;23 import com.drew.lang.RandomAccessReader; 24 24 import com.drew.lang.annotations.NotNull; 25 25 26 26 /** 27 * Interface through which all classes responsible for decoding a particular type of metadata may be called. 28 * Note that the data source is not specified on this interface. Instead it is suggested that implementations 29 * take their data within a constructor. Constructors might be overloaded to allow for different sources, such as 30 * files, streams and byte arrays. As such, instances of implementations of this interface would be single-use and 31 * not thread-safe. 27 * Defines an object capable of processing a particular type of metadata from a {@link RandomAccessReader}. 28 * <p> 29 * Instances of this interface must be thread-safe and reusable. 32 30 * 33 * @author Drew Noakes http://drewnoakes.com 31 * @author Drew Noakes https://drewnoakes.com 34 32 */ 35 33 public interface MetadataReader 36 34 { 37 35 /** 38 * Extract metadata from the sourceand merge it intoan existingMetadata object.36 * Extracts metadata from <code>reader</code> and merges it into the specified {@link Metadata} object. 39 37 * 40 * @param reader The readerfrom which the metadata should be extracted.41 * @param metadata The Metadataobject into which extracted values should be merged.38 * @param reader The {@link RandomAccessReader} from which the metadata should be extracted. 39 * @param metadata The {@link Metadata} object into which extracted values should be merged. 42 40 */ 43 public void extract(@NotNull final BufferReader reader, @NotNull final Metadata metadata);41 public void extract(@NotNull final RandomAccessReader reader, @NotNull final Metadata metadata); 44 42 } -
trunk/src/com/drew/metadata/Tag.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; … … 25 25 26 26 /** 27 * Models a particular tag within a directory and provides methods for obtaining its value.Note that a Tag instance is28 * specific to a particular metadata extraction and cannot be reused.27 * Models a particular tag within a {@link com.drew.metadata.Directory} and provides methods for obtaining its value. 28 * Immutable. 29 29 * 30 * @author Drew Noakes http://drewnoakes.com 30 * @author Drew Noakes https://drewnoakes.com 31 31 */ 32 32 public class Tag … … 79 79 80 80 /** 81 * Get whether this tag has a name. 82 * 83 * If <code>true</code>, it may be accessed via {@link #getTagName}. 84 * If <code>false</code>, {@link #getTagName} will return a string resembling <code>"Unknown tag (0x1234)"</code>. 85 * 86 * @return whether this tag has a name 87 */ 88 @NotNull 89 public boolean hasTagName() 90 { 91 return _directory.hasTagName(_tagType); 92 } 93 94 /** 81 95 * Get the name of the tag, such as <code>Aperture</code>, or 82 96 * <code>InteropVersion</code>. … … 91 105 92 106 /** 93 * Get the name of the directory in which the tag exists, such as107 * Get the name of the {@link com.drew.metadata.Directory} in which the tag exists, such as 94 108 * <code>Exif</code>, <code>GPS</code> or <code>Interoperability</code>. 95 109 * 96 * @return name of the directory in which this tag exists110 * @return name of the {@link com.drew.metadata.Directory} in which this tag exists 97 111 */ 98 112 @NotNull … … 107 121 * @return the tag's type and value 108 122 */ 123 @Override 109 124 @NotNull 110 125 public String toString() 111 126 { 112 127 String description = getDescription(); 113 if (description ==null)128 if (description == null) 114 129 description = _directory.getString(getTagType()) + " (unable to formulate description)"; 115 130 return "[" + _directory.getName() + "] " + getTagName() + " - " + description; -
trunk/src/com/drew/metadata/TagDescriptor.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata; 22 22 23 import com.drew.lang.Rational; 24 import com.drew.lang.StringUtil; 23 25 import com.drew.lang.annotations.NotNull; 24 26 import com.drew.lang.annotations.Nullable; 25 27 28 import java.io.UnsupportedEncodingException; 26 29 import java.lang.reflect.Array; 30 import java.util.ArrayList; 31 import java.util.Date; 32 import java.util.List; 27 33 28 34 /** 29 * Abstract base class for all tag descriptor classes. Implementations are responsible for35 * Base class for all tag descriptor classes. Implementations are responsible for 30 36 * providing the human-readable string representation of tag values stored in a directory. 31 37 * The directory is provided to the tag descriptor via its constructor. 32 38 * 33 * @author Drew Noakes http://drewnoakes.com 39 * @author Drew Noakes https://drewnoakes.com 34 40 */ 35 public abstractclass TagDescriptor<T extends Directory>41 public class TagDescriptor<T extends Directory> 36 42 { 37 43 @NotNull … … 44 50 45 51 /** 46 * Returns a descriptive value of the thespecified tag for this image.52 * Returns a descriptive value of the specified tag for this image. 47 53 * Where possible, known values will be substituted here in place of the raw 48 54 * tokens actually kept in the metadata segment. If no substitution is 49 55 * available, the value provided by <code>getString(tagType)</code> will be returned. 50 * 56 * 51 57 * @param tagType the tag to find a description for 52 58 * @return a description of the image's value for the specified tag, or … … 58 64 Object object = _directory.getObject(tagType); 59 65 60 if (object ==null)66 if (object == null) 61 67 return null; 62 68 … … 66 72 if (length > 16) { 67 73 final String componentTypeName = object.getClass().getComponentType().getName(); 68 return String.format("[%d %s%s]", length, componentTypeName, length ==1 ? "" : "s");74 return String.format("[%d %s%s]", length, componentTypeName, length == 1 ? "" : "s"); 69 75 } 70 76 } … … 77 83 * Takes a series of 4 bytes from the specified offset, and converts these to a 78 84 * well-known version number, where possible. 79 * <p />85 * <p> 80 86 * Two different formats are processed: 81 87 * <ul> 82 * 83 * 88 * <li>[30 32 31 30] -> 2.10</li> 89 * <li>[0 1 0 0] -> 1.00</li> 84 90 * </ul> 85 * @param components the four version values 86 * @param majorDigits the number of components to be 91 * 92 * @param components the four version values 93 * @param majorDigits the number of components to be 87 94 * @return the version as a string of form "2.10" or null if the argument cannot be converted 88 95 */ … … 90 97 public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits) 91 98 { 92 if (components ==null)99 if (components == null) 93 100 return null; 94 101 StringBuilder version = new StringBuilder(); … … 99 106 if (c < '0') 100 107 c += '0'; 101 if (i == 0 && c =='0')108 if (i == 0 && c == '0') 102 109 continue; 103 110 version.append(c); … … 105 112 return version.toString(); 106 113 } 114 115 @Nullable 116 protected String getVersionBytesDescription(final int tagType, int majorDigits) 117 { 118 int[] values = _directory.getIntArray(tagType); 119 return values == null ? null : convertBytesToVersionString(values, majorDigits); 120 } 121 122 @Nullable 123 protected String getIndexedDescription(final int tagType, @NotNull String... descriptions) 124 { 125 return getIndexedDescription(tagType, 0, descriptions); 126 } 127 128 @Nullable 129 protected String getIndexedDescription(final int tagType, final int baseIndex, @NotNull String... descriptions) 130 { 131 final Integer index = _directory.getInteger(tagType); 132 if (index == null) 133 return null; 134 final int arrayIndex = index - baseIndex; 135 if (arrayIndex >= 0 && arrayIndex < descriptions.length) { 136 String description = descriptions[arrayIndex]; 137 if (description != null) 138 return description; 139 } 140 return "Unknown (" + index + ")"; 141 } 142 143 @Nullable 144 protected String getByteLengthDescription(final int tagType) 145 { 146 byte[] bytes = _directory.getByteArray(tagType); 147 if (bytes == null) 148 return null; 149 return String.format("(%d byte%s)", bytes.length, bytes.length == 1 ? "" : "s"); 150 } 151 152 @Nullable 153 protected String getSimpleRational(final int tagType) 154 { 155 Rational value = _directory.getRational(tagType); 156 if (value == null) 157 return null; 158 return value.toSimpleString(true); 159 } 160 161 @Nullable 162 protected String getDecimalRational(final int tagType, final int decimalPlaces) 163 { 164 Rational value = _directory.getRational(tagType); 165 if (value == null) 166 return null; 167 return String.format("%." + decimalPlaces + "f", value.doubleValue()); 168 } 169 170 @Nullable 171 protected String getFormattedInt(final int tagType, @NotNull final String format) 172 { 173 Integer value = _directory.getInteger(tagType); 174 if (value == null) 175 return null; 176 return String.format(format, value); 177 } 178 179 @Nullable 180 protected String getFormattedFloat(final int tagType, @NotNull final String format) 181 { 182 Float value = _directory.getFloatObject(tagType); 183 if (value == null) 184 return null; 185 return String.format(format, value); 186 } 187 188 @Nullable 189 protected String getFormattedString(final int tagType, @NotNull final String format) 190 { 191 String value = _directory.getString(tagType); 192 if (value == null) 193 return null; 194 return String.format(format, value); 195 } 196 197 @Nullable 198 protected String getEpochTimeDescription(final int tagType) 199 { 200 // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?) 201 Long value = _directory.getLongObject(tagType); 202 if (value==null) 203 return null; 204 return new Date(value).toString(); 205 } 206 207 /** 208 * LSB first. Labels may be null, a String, or a String[2] with (low label,high label) values. 209 */ 210 @Nullable 211 protected String getBitFlagDescription(final int tagType, @NotNull final Object... labels) 212 { 213 Integer value = _directory.getInteger(tagType); 214 215 if (value == null) 216 return null; 217 218 List<String> parts = new ArrayList<String>(); 219 220 int bitIndex = 0; 221 while (labels.length > bitIndex) { 222 Object labelObj = labels[bitIndex]; 223 if (labelObj != null) { 224 boolean isBitSet = (value & 1) == 1; 225 if (labelObj instanceof String[]) { 226 String[] labelPair = (String[])labelObj; 227 assert(labelPair.length == 2); 228 parts.add(labelPair[isBitSet ? 1 : 0]); 229 } else if (isBitSet && labelObj instanceof String) { 230 parts.add((String)labelObj); 231 } 232 } 233 value >>= 1; 234 bitIndex++; 235 } 236 237 return StringUtil.join(parts, ", "); 238 } 239 240 @Nullable 241 protected String get7BitStringFromBytes(final int tagType) 242 { 243 final byte[] bytes = _directory.getByteArray(tagType); 244 245 if (bytes == null) 246 return null; 247 248 int length = bytes.length; 249 for (int index = 0; index < bytes.length; index++) { 250 int i = bytes[index] & 0xFF; 251 if (i == 0 || i > 0x7F) { 252 length = index; 253 break; 254 } 255 } 256 257 return new String(bytes, 0, length); 258 } 259 260 @Nullable 261 protected String getAsciiStringFromBytes(int tag) 262 { 263 byte[] values = _directory.getByteArray(tag); 264 265 if (values == null) 266 return null; 267 268 try { 269 return new String(values, "ASCII").trim(); 270 } catch (UnsupportedEncodingException e) { 271 return null; 272 } 273 } 107 274 } -
trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 29 29 import java.io.UnsupportedEncodingException; 30 30 31 import static com.drew.metadata.exif.ExifIFD0Directory.*; 32 31 33 /** 32 * Provides human-readable string representations of tag values stored in a <code>ExifIFD0Directory</code>.33 * 34 * @author Drew Noakes http://drewnoakes.com 34 * Provides human-readable string representations of tag values stored in a {@link ExifIFD0Directory}. 35 * 36 * @author Drew Noakes https://drewnoakes.com 35 37 */ 36 38 public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory> … … 54 56 55 57 /** 56 * Returns a descriptive value of the thespecified tag for this image.58 * Returns a descriptive value of the specified tag for this image. 57 59 * Where possible, known values will be substituted here in place of the raw 58 60 * tokens actually kept in the Exif segment. If no substitution is … … 62 64 * <code>null</code> if the tag hasn't been defined. 63 65 */ 66 @Override 64 67 @Nullable 65 68 public String getDescription(int tagType) 66 69 { 67 70 switch (tagType) { 68 case ExifIFD0Directory.TAG_RESOLUTION_UNIT:71 case TAG_RESOLUTION_UNIT: 69 72 return getResolutionDescription(); 70 case ExifIFD0Directory.TAG_YCBCR_POSITIONING:73 case TAG_YCBCR_POSITIONING: 71 74 return getYCbCrPositioningDescription(); 72 case ExifIFD0Directory.TAG_X_RESOLUTION:75 case TAG_X_RESOLUTION: 73 76 return getXResolutionDescription(); 74 case ExifIFD0Directory.TAG_Y_RESOLUTION:77 case TAG_Y_RESOLUTION: 75 78 return getYResolutionDescription(); 76 case ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE:79 case TAG_REFERENCE_BLACK_WHITE: 77 80 return getReferenceBlackWhiteDescription(); 78 case ExifIFD0Directory.TAG_ORIENTATION:81 case TAG_ORIENTATION: 79 82 return getOrientationDescription(); 80 83 81 case ExifIFD0Directory.TAG_WIN_AUTHOR:84 case TAG_WIN_AUTHOR: 82 85 return getWindowsAuthorDescription(); 83 case ExifIFD0Directory.TAG_WIN_COMMENT:86 case TAG_WIN_COMMENT: 84 87 return getWindowsCommentDescription(); 85 case ExifIFD0Directory.TAG_WIN_KEYWORDS:88 case TAG_WIN_KEYWORDS: 86 89 return getWindowsKeywordsDescription(); 87 case ExifIFD0Directory.TAG_WIN_SUBJECT:90 case TAG_WIN_SUBJECT: 88 91 return getWindowsSubjectDescription(); 89 case ExifIFD0Directory.TAG_WIN_TITLE:92 case TAG_WIN_TITLE: 90 93 return getWindowsTitleDescription(); 91 94 … … 98 101 public String getReferenceBlackWhiteDescription() 99 102 { 100 int[] ints = _directory.getIntArray( ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE);101 if (ints==null) 103 int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE); 104 if (ints==null || ints.length < 6) 102 105 return null; 103 106 int blackR = ints[0]; … … 107 110 int blackB = ints[4]; 108 111 int whiteB = ints[5]; 109 return "[" + blackR + "," + blackG + "," + blackB + "] " + 110 "[" + whiteR + "," + whiteG + "," + whiteB + "]"; 112 return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB); 111 113 } 112 114 … … 114 116 public String getYResolutionDescription() 115 117 { 116 Rational value = _directory.getRational( ExifIFD0Directory.TAG_Y_RESOLUTION);118 Rational value = _directory.getRational(TAG_Y_RESOLUTION); 117 119 if (value==null) 118 120 return null; 119 121 final String unit = getResolutionDescription(); 120 return value.toSimpleString(_allowDecimalRepresentationOfRationals) +121 " dots per " +122 (unit==null ? "unit" : unit.toLowerCase());122 return String.format("%s dots per %s", 123 value.toSimpleString(_allowDecimalRepresentationOfRationals), 124 unit == null ? "unit" : unit.toLowerCase()); 123 125 } 124 126 … … 126 128 public String getXResolutionDescription() 127 129 { 128 Rational value = _directory.getRational( ExifIFD0Directory.TAG_X_RESOLUTION);130 Rational value = _directory.getRational(TAG_X_RESOLUTION); 129 131 if (value==null) 130 132 return null; 131 133 final String unit = getResolutionDescription(); 132 return value.toSimpleString(_allowDecimalRepresentationOfRationals) +133 " dots per " +134 (unit==null ? "unit" : unit.toLowerCase());134 return String.format("%s dots per %s", 135 value.toSimpleString(_allowDecimalRepresentationOfRationals), 136 unit == null ? "unit" : unit.toLowerCase()); 135 137 } 136 138 … … 138 140 public String getYCbCrPositioningDescription() 139 141 { 140 Integer value = _directory.getInteger(ExifIFD0Directory.TAG_YCBCR_POSITIONING); 141 if (value==null) 142 return null; 143 switch (value) { 144 case 1: return "Center of pixel array"; 145 case 2: return "Datum point"; 146 default: 147 return String.valueOf(value); 142 return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point"); 143 } 144 145 @Nullable 146 public String getOrientationDescription() 147 { 148 return getIndexedDescription(TAG_ORIENTATION, 1, 149 "Top, left side (Horizontal / normal)", 150 "Top, right side (Mirror horizontal)", 151 "Bottom, right side (Rotate 180)", 152 "Bottom, left side (Mirror vertical)", 153 "Left side, top (Mirror horizontal and rotate 270 CW)", 154 "Right side, top (Rotate 90 CW)", 155 "Right side, bottom (Mirror horizontal and rotate 90 CW)", 156 "Left side, bottom (Rotate 270 CW)"); 157 } 158 159 @Nullable 160 public String getResolutionDescription() 161 { 162 // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch) 163 return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm"); 164 } 165 166 /** The Windows specific tags uses plain Unicode. */ 167 @Nullable 168 private String getUnicodeDescription(int tag) 169 { 170 byte[] bytes = _directory.getByteArray(tag); 171 if (bytes == null) 172 return null; 173 try { 174 // Decode the unicode string and trim the unicode zero "\0" from the end. 175 return new String(bytes, "UTF-16LE").trim(); 176 } catch (UnsupportedEncodingException ex) { 177 return null; 148 178 } 149 179 } 150 180 151 181 @Nullable 152 public String getOrientationDescription()153 {154 Integer value = _directory.getInteger(ExifIFD0Directory.TAG_ORIENTATION);155 if (value==null)156 return null;157 switch (value) {158 case 1: return "Top, left side (Horizontal / normal)";159 case 2: return "Top, right side (Mirror horizontal)";160 case 3: return "Bottom, right side (Rotate 180)";161 case 4: return "Bottom, left side (Mirror vertical)";162 case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";163 case 6: return "Right side, top (Rotate 90 CW)";164 case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";165 case 8: return "Left side, bottom (Rotate 270 CW)";166 default:167 return String.valueOf(value);168 }169 }170 171 @Nullable172 public String getResolutionDescription()173 {174 // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)175 Integer value = _directory.getInteger(ExifIFD0Directory.TAG_RESOLUTION_UNIT);176 if (value==null)177 return null;178 switch (value) {179 case 1: return "(No unit)";180 case 2: return "Inch";181 case 3: return "cm";182 default:183 return "";184 }185 }186 187 /** The Windows specific tags uses plain Unicode. */188 @Nullable189 private String getUnicodeDescription(int tag)190 {191 byte[] commentBytes = _directory.getByteArray(tag);192 if (commentBytes==null)193 return null;194 try {195 // Decode the unicode string and trim the unicode zero "\0" from the end.196 return new String(commentBytes, "UTF-16LE").trim();197 }198 catch (UnsupportedEncodingException ex) {199 return null;200 }201 }202 203 @Nullable204 182 public String getWindowsAuthorDescription() 205 183 { 206 return getUnicodeDescription( ExifIFD0Directory.TAG_WIN_AUTHOR);184 return getUnicodeDescription(TAG_WIN_AUTHOR); 207 185 } 208 186 … … 210 188 public String getWindowsCommentDescription() 211 189 { 212 return getUnicodeDescription( ExifIFD0Directory.TAG_WIN_COMMENT);190 return getUnicodeDescription(TAG_WIN_COMMENT); 213 191 } 214 192 … … 216 194 public String getWindowsKeywordsDescription() 217 195 { 218 return getUnicodeDescription( ExifIFD0Directory.TAG_WIN_KEYWORDS);196 return getUnicodeDescription(TAG_WIN_KEYWORDS); 219 197 } 220 198 … … 222 200 public String getWindowsTitleDescription() 223 201 { 224 return getUnicodeDescription( ExifIFD0Directory.TAG_WIN_TITLE);202 return getUnicodeDescription(TAG_WIN_TITLE); 225 203 } 226 204 … … 228 206 public String getWindowsSubjectDescription() 229 207 { 230 return getUnicodeDescription( ExifIFD0Directory.TAG_WIN_SUBJECT);208 return getUnicodeDescription(TAG_WIN_SUBJECT); 231 209 } 232 210 } -
trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 30 30 * Describes Exif tags from the IFD0 directory. 31 31 * 32 * @author Drew Noakes http://drewnoakes.com 32 * @author Drew Noakes https://drewnoakes.com 33 33 */ 34 34 public class ExifIFD0Directory extends Directory … … 46 46 public static final int TAG_WHITE_POINT = 0x013E; 47 47 public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F; 48 48 49 public static final int TAG_YCBCR_COEFFICIENTS = 0x0211; 49 50 public static final int TAG_YCBCR_POSITIONING = 0x0213; 50 51 public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214; 52 53 54 /** This tag is a pointer to the Exif SubIFD. */ 55 public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769; 56 57 /** This tag is a pointer to the Exif GPS IFD. */ 58 public static final int TAG_GPS_INFO_OFFSET = 0x8825; 59 51 60 public static final int TAG_COPYRIGHT = 0x8298; 61 62 /** Non-standard, but in use. */ 63 public static final int TAG_TIME_ZONE_OFFSET = 0x882a; 52 64 53 65 /** The image title, as used by Windows XP. */ … … 82 94 _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning"); 83 95 _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White"); 96 84 97 _tagNameMap.put(TAG_COPYRIGHT, "Copyright"); 98 99 _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset"); 85 100 86 101 _tagNameMap.put(TAG_WIN_AUTHOR, "Windows XP Author"); … … 96 111 } 97 112 113 @Override 98 114 @NotNull 99 115 public String getName() … … 102 118 } 103 119 120 @Override 104 121 @NotNull 105 122 protected HashMap<Integer, String> getTagNameMap() -
trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata.exif; … … 25 25 import com.drew.metadata.TagDescriptor; 26 26 27 import static com.drew.metadata.exif.ExifInteropDirectory.*; 28 27 29 /** 28 * Provides human-readable string representations of tag values stored in a <code>ExifInteropDirectory</code>.30 * Provides human-readable string representations of tag values stored in a {@link ExifInteropDirectory}. 29 31 * 30 * @author Drew Noakes http://drewnoakes.com 32 * @author Drew Noakes https://drewnoakes.com 31 33 */ 32 34 public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory> … … 37 39 } 38 40 41 @Override 39 42 @Nullable 40 43 public String getDescription(int tagType) 41 44 { 42 45 switch (tagType) { 43 case ExifInteropDirectory.TAG_INTEROP_INDEX:46 case TAG_INTEROP_INDEX: 44 47 return getInteropIndexDescription(); 45 case ExifInteropDirectory.TAG_INTEROP_VERSION:48 case TAG_INTEROP_VERSION: 46 49 return getInteropVersionDescription(); 47 50 default: … … 53 56 public String getInteropVersionDescription() 54 57 { 55 int[] ints = _directory.getIntArray(ExifInteropDirectory.TAG_INTEROP_VERSION); 56 return convertBytesToVersionString(ints, 2); 58 return getVersionBytesDescription(TAG_INTEROP_VERSION, 2); 57 59 } 58 60 … … 60 62 public String getInteropIndexDescription() 61 63 { 62 String value = _directory.getString( ExifInteropDirectory.TAG_INTEROP_INDEX);64 String value = _directory.getString(TAG_INTEROP_INDEX); 63 65 64 if (value ==null)66 if (value == null) 65 67 return null; 66 68 -
trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata.exif; … … 29 29 * Describes Exif interoperability tags. 30 30 * 31 * @author Drew Noakes http://drewnoakes.com 31 * @author Drew Noakes https://drewnoakes.com 32 32 */ 33 33 public class ExifInteropDirectory extends Directory … … 56 56 } 57 57 58 @Override 58 59 @NotNull 59 60 public String getName() … … 62 63 } 63 64 65 @Override 64 66 @NotNull 65 67 protected HashMap<Integer, String> getTagNameMap() -
trunk/src/com/drew/metadata/exif/ExifReader.java
r6209 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata.exif; 22 22 23 import java.util.HashSet; 24 import java.util.Set; 23 import com.drew.imaging.jpeg.JpegSegmentMetadataReader; 24 import com.drew.imaging.jpeg.JpegSegmentType; 25 import com.drew.imaging.tiff.TiffProcessingException; 26 import com.drew.imaging.tiff.TiffReader; 27 import com.drew.lang.ByteArrayReader; 28 import com.drew.lang.annotations.NotNull; 29 import com.drew.metadata.Metadata; 25 30 26 import com.drew.lang.BufferBoundsException; 27 import com.drew.lang.BufferReader; 28 import com.drew.lang.Rational; 29 import com.drew.lang.annotations.NotNull; 30 import com.drew.metadata.Directory; 31 import com.drew.metadata.Metadata; 32 import com.drew.metadata.MetadataReader; 31 import java.io.IOException; 32 import java.util.Arrays; 33 33 34 34 /** 35 35 * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory}, 36 * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera makernote directories. 36 * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera 37 * makernote directories. 37 38 * 38 * @author Drew Noakes http://drewnoakes.com 39 * @author Drew Noakes https://drewnoakes.com 39 40 */ 40 public class ExifReader implements MetadataReader 41 public class ExifReader implements JpegSegmentMetadataReader 41 42 { 42 // TODO extract a reusable TiffReader from this class with hooks for special tag handling and subdir following 43 44 /** The number of bytes used per format descriptor. */ 43 /** 44 * The offset at which the TIFF data actually starts. This may be necessary when, for example, processing 45 * JPEG Exif data from APP0 which has a 6-byte preamble before starting the TIFF data. 46 */ 47 private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0"; 48 49 private boolean _storeThumbnailBytes = true; 50 51 public boolean isStoreThumbnailBytes() 52 { 53 return _storeThumbnailBytes; 54 } 55 56 public void setStoreThumbnailBytes(boolean storeThumbnailBytes) 57 { 58 _storeThumbnailBytes = storeThumbnailBytes; 59 } 60 45 61 @NotNull 46 private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 }; 62 public Iterable<JpegSegmentType> getSegmentTypes() 63 { 64 return Arrays.asList(JpegSegmentType.APP1); 65 } 47 66 48 /** The number of formats known. */ 49 private static final int MAX_FORMAT_CODE = 12; 67 public boolean canProcess(@NotNull final byte[] segmentBytes, @NotNull final JpegSegmentType segmentType) 68 { 69 return segmentBytes.length >= JPEG_EXIF_SEGMENT_PREAMBLE.length() && new String(segmentBytes, 0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equalsIgnoreCase(JPEG_EXIF_SEGMENT_PREAMBLE); 70 } 50 71 51 // Format types 52 // TODO use an enum for these? 53 /** An 8-bit unsigned integer. */ 54 private static final int FMT_BYTE = 1; 55 /** A fixed-length character string. */ 56 private static final int FMT_STRING = 2; 57 /** An unsigned 16-bit integer. */ 58 private static final int FMT_USHORT = 3; 59 /** An unsigned 32-bit integer. */ 60 private static final int FMT_ULONG = 4; 61 private static final int FMT_URATIONAL = 5; 62 /** An 8-bit signed integer. */ 63 private static final int FMT_SBYTE = 6; 64 private static final int FMT_UNDEFINED = 7; 65 /** A signed 16-bit integer. */ 66 private static final int FMT_SSHORT = 8; 67 /** A signed 32-bit integer. */ 68 private static final int FMT_SLONG = 9; 69 private static final int FMT_SRATIONAL = 10; 70 /** A 32-bit floating point number. */ 71 private static final int FMT_SINGLE = 11; 72 /** A 64-bit floating point number. */ 73 private static final int FMT_DOUBLE = 12; 72 public void extract(@NotNull final byte[] segmentBytes, @NotNull final Metadata metadata, @NotNull final JpegSegmentType segmentType) 73 { 74 if (segmentBytes == null) 75 throw new NullPointerException("segmentBytes cannot be null"); 76 if (metadata == null) 77 throw new NullPointerException("metadata cannot be null"); 78 if (segmentType == null) 79 throw new NullPointerException("segmentType cannot be null"); 74 80 75 /** This tag is a pointer to the Exif SubIFD. */ 76 public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769; 77 /** This tag is a pointer to the Exif Interop IFD. */ 78 public static final int TAG_INTEROP_OFFSET = 0xA005; 79 /** This tag is a pointer to the Exif GPS IFD. */ 80 public static final int TAG_GPS_INFO_OFFSET = 0x8825; 81 /** This tag is a pointer to the Exif Makernote IFD. */ 82 public static final int TAG_MAKER_NOTE_OFFSET = 0x927C; 81 try { 82 ByteArrayReader reader = new ByteArrayReader(segmentBytes); 83 83 84 public static final int TIFF_HEADER_START_OFFSET = 6; 85 86 /** 87 * Performs the Exif data extraction, adding found values to the specified 88 * instance of <code>Metadata</code>. 89 * 90 * @param reader The buffer reader from which Exif data should be read. 91 * @param metadata The Metadata object into which extracted values should be merged. 92 */ 93 public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata) 94 { 95 final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class); 96 97 // check for the header length 98 if (reader.getLength() <= 14) { 99 directory.addError("Exif data segment must contain at least 14 bytes"); 100 return; 101 } 102 103 // check for the header preamble 104 try { 105 if (!reader.getString(0, 6).equals("Exif\0\0")) { 106 directory.addError("Exif data segment doesn't begin with 'Exif'"); 84 // 85 // Check for the header preamble 86 // 87 try { 88 if (!reader.getString(0, JPEG_EXIF_SEGMENT_PREAMBLE.length()).equals(JPEG_EXIF_SEGMENT_PREAMBLE)) { 89 // TODO what do to with this error state? 90 System.err.println("Invalid JPEG Exif segment preamble"); 91 return; 92 } 93 } catch (IOException e) { 94 // TODO what do to with this error state? 95 e.printStackTrace(System.err); 107 96 return; 108 97 } 109 98 110 extractIFD(metadata, metadata.getOrCreateDirectory(ExifIFD0Directory.class), TIFF_HEADER_START_OFFSET, reader); 111 } catch (BufferBoundsException e) { 112 directory.addError("Exif data segment ended prematurely"); 99 // 100 // Read the TIFF-formatted Exif data 101 // 102 new TiffReader().processTiff( 103 reader, 104 new ExifTiffHandler(metadata, _storeThumbnailBytes), 105 JPEG_EXIF_SEGMENT_PREAMBLE.length() 106 ); 107 108 } catch (TiffProcessingException e) { 109 // TODO what do to with this error state? 110 e.printStackTrace(System.err); 111 } catch (IOException e) { 112 // TODO what do to with this error state? 113 e.printStackTrace(System.err); 113 114 } 114 115 } 115 116 /**117 * Performs the Exif data extraction on a TIFF/RAW, adding found values to the specified118 * instance of <code>Metadata</code>.119 *120 * @param reader The BufferReader from which TIFF data should be read.121 * @param metadata The Metadata object into which extracted values should be merged.122 */123 public void extractTiff(@NotNull BufferReader reader, @NotNull Metadata metadata)124 {125 final ExifIFD0Directory directory = metadata.getOrCreateDirectory(ExifIFD0Directory.class);126 127 try {128 extractIFD(metadata, directory, 0, reader);129 } catch (BufferBoundsException e) {130 directory.addError("Exif data segment ended prematurely");131 }132 }133 134 private void extractIFD(@NotNull Metadata metadata, @NotNull final ExifIFD0Directory directory, int tiffHeaderOffset, @NotNull BufferReader reader) throws BufferBoundsException135 {136 // this should be either "MM" or "II"137 String byteOrderIdentifier = reader.getString(tiffHeaderOffset, 2);138 139 if ("MM".equals(byteOrderIdentifier)) {140 reader.setMotorolaByteOrder(true);141 } else if ("II".equals(byteOrderIdentifier)) {142 reader.setMotorolaByteOrder(false);143 } else {144 directory.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);145 return;146 }147 148 // Check the next two values for correctness.149 final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset);150 151 final int standardTiffMarker = 0x002A;152 final int olympusRawTiffMarker = 0x4F52; // for ORF files153 final int panasonicRawTiffMarker = 0x0055; // for RW2 files154 155 if (tiffMarker != standardTiffMarker && tiffMarker != olympusRawTiffMarker && tiffMarker != panasonicRawTiffMarker) {156 directory.addError("Unexpected TIFF marker after byte order identifier: 0x" + Integer.toHexString(tiffMarker));157 return;158 }159 160 int firstDirectoryOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;161 162 // David Ekholm sent a digital camera image that has this problem163 if (firstDirectoryOffset >= reader.getLength() - 1) {164 directory.addError("First exif directory offset is beyond end of Exif data segment");165 // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case166 firstDirectoryOffset = 14;167 }168 169 Set<Integer> processedDirectoryOffsets = new HashSet<Integer>();170 171 processDirectory(directory, processedDirectoryOffsets, firstDirectoryOffset, tiffHeaderOffset, metadata, reader);172 173 // after the extraction process, if we have the correct tags, we may be able to store thumbnail information174 ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);175 if (thumbnailDirectory!=null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {176 Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);177 Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);178 if (offset != null && length != null) {179 try {180 byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);181 thumbnailDirectory.setThumbnailData(thumbnailData);182 } catch (BufferBoundsException ex) {183 directory.addError("Invalid thumbnail data specification: " + ex.getMessage());184 }185 }186 }187 }188 189 /**190 * Process one of the nested Tiff IFD directories.191 * <p/>192 * Header193 * 2 bytes: number of tags194 * <p/>195 * Then for each tag196 * 2 bytes: tag type197 * 2 bytes: format code198 * 4 bytes: component count199 */200 private void processDirectory(@NotNull Directory directory, @NotNull Set<Integer> processedDirectoryOffsets, int dirStartOffset, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull final BufferReader reader) throws BufferBoundsException201 {202 // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist203 if (processedDirectoryOffsets.contains(Integer.valueOf(dirStartOffset)))204 return;205 206 // remember that we've visited this directory so that we don't visit it again later207 processedDirectoryOffsets.add(dirStartOffset);208 209 if (dirStartOffset >= reader.getLength() || dirStartOffset < 0) {210 directory.addError("Ignored directory marked to start outside data segment");211 return;212 }213 214 // First two bytes in the IFD are the number of tags in this directory215 int dirTagCount = reader.getUInt16(dirStartOffset);216 217 int dirLength = (2 + (12 * dirTagCount) + 4);218 if (dirLength + dirStartOffset > reader.getLength()) {219 directory.addError("Illegally sized directory");220 return;221 }222 223 // Handle each tag in this directory224 for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {225 final int tagOffset = calculateTagOffset(dirStartOffset, tagNumber);226 227 // 2 bytes for the tag type228 final int tagType = reader.getUInt16(tagOffset);229 230 // 2 bytes for the format code231 final int formatCode = reader.getUInt16(tagOffset + 2);232 if (formatCode < 1 || formatCode > MAX_FORMAT_CODE) {233 // This error suggests that we are processing at an incorrect index and will generate234 // rubbish until we go out of bounds (which may be a while). Exit now.235 directory.addError("Invalid TIFF tag format code: " + formatCode);236 continue; // JOSM patch to fix #9030237 }238 239 // 4 bytes dictate the number of components in this tag's data240 final int componentCount = reader.getInt32(tagOffset + 4);241 if (componentCount < 0) {242 directory.addError("Negative TIFF tag component count");243 continue;244 }245 // each component may have more than one byte... calculate the total number of bytes246 final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];247 final int tagValueOffset;248 if (byteCount > 4) {249 // If it's bigger than 4 bytes, the dir entry contains an offset.250 // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an251 // offset relative to the start of the makernote itself, not the TIFF segment.252 final int offsetVal = reader.getInt32(tagOffset + 8);253 if (offsetVal + byteCount > reader.getLength()) {254 // Bogus pointer offset and / or byteCount value255 directory.addError("Illegal TIFF tag pointer offset");256 continue;257 }258 tagValueOffset = tiffHeaderOffset + offsetVal;259 } else {260 // 4 bytes or less and value is in the dir entry itself261 tagValueOffset = tagOffset + 8;262 }263 264 if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) {265 directory.addError("Illegal TIFF tag pointer offset");266 continue;267 }268 269 // Check that this tag isn't going to allocate outside the bounds of the data array.270 // This addresses an uncommon OutOfMemoryError.271 if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) {272 directory.addError("Illegal number of bytes: " + byteCount);273 continue;274 }275 276 switch (tagType) {277 case TAG_EXIF_SUB_IFD_OFFSET: {278 final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);279 processDirectory(metadata.getOrCreateDirectory(ExifSubIFDDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);280 continue;281 }282 case TAG_INTEROP_OFFSET: {283 final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);284 processDirectory(metadata.getOrCreateDirectory(ExifInteropDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);285 continue;286 }287 case TAG_GPS_INFO_OFFSET: {288 final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);289 processDirectory(metadata.getOrCreateDirectory(GpsDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);290 continue;291 }292 case TAG_MAKER_NOTE_OFFSET: {293 processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset, metadata, reader);294 continue;295 }296 default: {297 processTag(directory, tagType, tagValueOffset, componentCount, formatCode, reader);298 break;299 }300 }301 }302 303 // at the end of each IFD is an optional link to the next IFD304 final int finalTagOffset = calculateTagOffset(dirStartOffset, dirTagCount);305 int nextDirectoryOffset = reader.getInt32(finalTagOffset);306 if (nextDirectoryOffset != 0) {307 nextDirectoryOffset += tiffHeaderOffset;308 if (nextDirectoryOffset >= reader.getLength()) {309 // Last 4 bytes of IFD reference another IFD with an address that is out of bounds310 // Note this could have been caused by jhead 1.3 cropping too much311 return;312 } else if (nextDirectoryOffset < dirStartOffset) {313 // Last 4 bytes of IFD reference another IFD with an address that is before the start of this directory314 return;315 }316 // TODO in Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case317 final ExifThumbnailDirectory nextDirectory = metadata.getOrCreateDirectory(ExifThumbnailDirectory.class);318 processDirectory(nextDirectory, processedDirectoryOffsets, nextDirectoryOffset, tiffHeaderOffset, metadata, reader);319 }320 }321 322 private void processMakerNote(int subdirOffset, @NotNull Set<Integer> processedDirectoryOffsets, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull BufferReader reader) throws BufferBoundsException323 {324 // Determine the camera model and makernote format325 Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class);326 327 if (ifd0Directory==null)328 return;329 330 String cameraModel = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);331 332 //final String firstTwoChars = reader.getString(subdirOffset, 2);333 final String firstThreeChars = reader.getString(subdirOffset, 3);334 final String firstFourChars = reader.getString(subdirOffset, 4);335 final String firstFiveChars = reader.getString(subdirOffset, 5);336 final String firstSixChars = reader.getString(subdirOffset, 6);337 final String firstSevenChars = reader.getString(subdirOffset, 7);338 final String firstEightChars = reader.getString(subdirOffset, 8);339 final String firstTwelveChars = reader.getString(subdirOffset, 12);340 341 if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {342 // Olympus Makernote343 // Epson and Agfa use Olympus maker note standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/344 processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);345 } else if (cameraModel != null && cameraModel.trim().toUpperCase().startsWith("NIKON")) {346 if ("Nikon".equals(firstFiveChars)) {347 /* There are two scenarios here:348 * Type 1: **349 * :0000: 4E 69 6B 6F 6E 00 01 00-05 00 02 00 02 00 06 00 Nikon...........350 * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................351 * Type 3: **352 * :0000: 4E 69 6B 6F 6E 00 02 00-00 00 4D 4D 00 2A 00 00 Nikon....MM.*...353 * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200354 */355 switch (reader.getUInt8(subdirOffset + 6)) {356 case 1:357 processDirectory(metadata.getOrCreateDirectory(NikonType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);358 break;359 case 2:360 processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 18, subdirOffset + 10, metadata, reader);361 break;362 default:363 ifd0Directory.addError("Unsupported Nikon makernote data ignored.");364 break;365 }366 } else {367 // The IFD begins with the first MakerNote byte (no ASCII name). This occurs with CoolPix 775, E990 and D1 models.368 processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);369 }370 } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {371 processDirectory(metadata.getOrCreateDirectory(SonyType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);372 } else if ("SIGMA\u0000\u0000\u0000".equals(firstEightChars) || "FOVEON\u0000\u0000".equals(firstEightChars)) {373 processDirectory(metadata.getOrCreateDirectory(SigmaMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 10, tiffHeaderOffset, metadata, reader);374 } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {375 // force MM for this directory376 boolean isMotorola = reader.isMotorolaByteOrder();377 reader.setMotorolaByteOrder(true);378 // skip 12 byte header + 2 for "MM" + 6379 processDirectory(metadata.getOrCreateDirectory(SonyType6MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);380 reader.setMotorolaByteOrder(isMotorola);381 } else if ("KDK".equals(firstThreeChars)) {382 processDirectory(metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);383 } else if ("Canon".equalsIgnoreCase(cameraModel)) {384 processDirectory(metadata.getOrCreateDirectory(CanonMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);385 } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("CASIO")) {386 if ("QVC\u0000\u0000\u0000".equals(firstSixChars))387 processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, tiffHeaderOffset, metadata, reader);388 else389 processDirectory(metadata.getOrCreateDirectory(CasioType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);390 } else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraModel)) {391 boolean byteOrderBefore = reader.isMotorolaByteOrder();392 // bug in fujifilm makernote ifd means we temporarily use Intel byte ordering393 reader.setMotorolaByteOrder(false);394 // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote395 // IFD, though the offset is relative to the start of the makernote, not the TIFF396 // header (like everywhere else)397 int ifdStart = subdirOffset + reader.getInt32(subdirOffset + 8);398 processDirectory(metadata.getOrCreateDirectory(FujifilmMakernoteDirectory.class), processedDirectoryOffsets, ifdStart, tiffHeaderOffset, metadata, reader);399 reader.setMotorolaByteOrder(byteOrderBefore);400 } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("MINOLTA")) {401 // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote402 // area that commences immediately.403 processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);404 } else if ("KYOCERA".equals(firstSevenChars)) {405 // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html406 processDirectory(metadata.getOrCreateDirectory(KyoceraMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 22, tiffHeaderOffset, metadata, reader);407 } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(subdirOffset, 12))) {408 // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD409 // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment410 // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html411 processDirectory(metadata.getOrCreateDirectory(PanasonicMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);412 } else if ("AOC\u0000".equals(firstFourChars)) {413 // NON-Standard TIFF IFD Data using Casio Type 2 Tags414 // IFD has no Next-IFD pointer at end of IFD, and415 // Offsets are relative to the start of the current IFD tag, not the TIFF header416 // Observed for:417 // - Pentax ist D418 processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, subdirOffset, metadata, reader);419 } else if (cameraModel != null && (cameraModel.toUpperCase().startsWith("PENTAX") || cameraModel.toUpperCase().startsWith("ASAHI"))) {420 // NON-Standard TIFF IFD Data using Pentax Tags421 // IFD has no Next-IFD pointer at end of IFD, and422 // Offsets are relative to the start of the current IFD tag, not the TIFF header423 // Observed for:424 // - PENTAX Optio 330425 // - PENTAX Optio 430426 processDirectory(metadata.getOrCreateDirectory(PentaxMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, subdirOffset, metadata, reader);427 // } else if ("KC".equals(firstTwoChars) || "MINOL".equals(firstFiveChars) || "MLY".equals(firstThreeChars) || "+M+M+M+M".equals(firstEightChars)) {428 // // This Konica data is not understood. Header identified in accordance with information at this site:429 // // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html430 // // TODO add support for minolta/konica cameras431 // exifDirectory.addError("Unsupported Konica/Minolta data ignored.");432 } else {433 // TODO how to store makernote data when it's not from a supported camera model?434 // this is difficult as the starting offset is not known. we could look for it...435 }436 }437 438 private void processTag(@NotNull Directory directory, int tagType, int tagValueOffset, int componentCount, int formatCode, @NotNull final BufferReader reader) throws BufferBoundsException439 {440 // Directory simply stores raw values441 // The display side uses a Descriptor class per directory to turn the raw values into 'pretty' descriptions442 switch (formatCode) {443 case FMT_UNDEFINED:444 // this includes exif user comments445 directory.setByteArray(tagType, reader.getBytes(tagValueOffset, componentCount));446 break;447 case FMT_STRING:448 String string = reader.getNullTerminatedString(tagValueOffset, componentCount);449 directory.setString(tagType, string);450 /*451 // special handling for certain known tags, proposed by Yuri Binev but left out for now,452 // as it gives the false impression that the image was captured in the same timezone453 // in which the string is parsed454 if (tagType==ExifSubIFDDirectory.TAG_DATETIME ||455 tagType==ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL ||456 tagType==ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED) {457 String[] datePatterns = {458 "yyyy:MM:dd HH:mm:ss",459 "yyyy:MM:dd HH:mm",460 "yyyy-MM-dd HH:mm:ss",461 "yyyy-MM-dd HH:mm"};462 for (String datePattern : datePatterns) {463 try {464 DateFormat parser = new SimpleDateFormat(datePattern);465 Date date = parser.parse(string);466 directory.setDate(tagType, date);467 break;468 } catch (ParseException ex) {469 // simply try the next pattern470 }471 }472 }473 */474 break;475 case FMT_SRATIONAL:476 if (componentCount == 1) {477 directory.setRational(tagType, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));478 } else if (componentCount > 1) {479 Rational[] rationals = new Rational[componentCount];480 for (int i = 0; i < componentCount; i++)481 rationals[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));482 directory.setRationalArray(tagType, rationals);483 }484 break;485 case FMT_URATIONAL:486 if (componentCount == 1) {487 directory.setRational(tagType, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));488 } else if (componentCount > 1) {489 Rational[] rationals = new Rational[componentCount];490 for (int i = 0; i < componentCount; i++)491 rationals[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));492 directory.setRationalArray(tagType, rationals);493 }494 break;495 case FMT_SINGLE:496 if (componentCount == 1) {497 directory.setFloat(tagType, reader.getFloat32(tagValueOffset));498 } else {499 float[] floats = new float[componentCount];500 for (int i = 0; i < componentCount; i++)501 floats[i] = reader.getFloat32(tagValueOffset + (i * 4));502 directory.setFloatArray(tagType, floats);503 }504 break;505 case FMT_DOUBLE:506 if (componentCount == 1) {507 directory.setDouble(tagType, reader.getDouble64(tagValueOffset));508 } else {509 double[] doubles = new double[componentCount];510 for (int i = 0; i < componentCount; i++)511 doubles[i] = reader.getDouble64(tagValueOffset + (i * 4));512 directory.setDoubleArray(tagType, doubles);513 }514 break;515 516 //517 // Note that all integral types are stored as int32 internally (the largest supported by TIFF)518 //519 520 case FMT_SBYTE:521 if (componentCount == 1) {522 directory.setInt(tagType, reader.getInt8(tagValueOffset));523 } else {524 int[] bytes = new int[componentCount];525 for (int i = 0; i < componentCount; i++)526 bytes[i] = reader.getInt8(tagValueOffset + i);527 directory.setIntArray(tagType, bytes);528 }529 break;530 case FMT_BYTE:531 if (componentCount == 1) {532 directory.setInt(tagType, reader.getUInt8(tagValueOffset));533 } else {534 int[] bytes = new int[componentCount];535 for (int i = 0; i < componentCount; i++)536 bytes[i] = reader.getUInt8(tagValueOffset + i);537 directory.setIntArray(tagType, bytes);538 }539 break;540 case FMT_USHORT:541 if (componentCount == 1) {542 int i = reader.getUInt16(tagValueOffset);543 directory.setInt(tagType, i);544 } else {545 int[] ints = new int[componentCount];546 for (int i = 0; i < componentCount; i++)547 ints[i] = reader.getUInt16(tagValueOffset + (i * 2));548 directory.setIntArray(tagType, ints);549 }550 break;551 case FMT_SSHORT:552 if (componentCount == 1) {553 int i = reader.getInt16(tagValueOffset);554 directory.setInt(tagType, i);555 } else {556 int[] ints = new int[componentCount];557 for (int i = 0; i < componentCount; i++)558 ints[i] = reader.getInt16(tagValueOffset + (i * 2));559 directory.setIntArray(tagType, ints);560 }561 break;562 case FMT_SLONG:563 case FMT_ULONG:564 // NOTE 'long' in this case means 32 bit, not 64565 if (componentCount == 1) {566 int i = reader.getInt32(tagValueOffset);567 directory.setInt(tagType, i);568 } else {569 int[] ints = new int[componentCount];570 for (int i = 0; i < componentCount; i++)571 ints[i] = reader.getInt32(tagValueOffset + (i * 4));572 directory.setIntArray(tagType, ints);573 }574 break;575 default:576 directory.addError("Unknown format code " + formatCode + " for tag " + tagType);577 }578 }579 580 /**581 * Determine the offset at which a given InteropArray entry begins within the specified IFD.582 *583 * @param dirStartOffset the offset at which the IFD starts584 * @param entryNumber the zero-based entry number585 */586 private int calculateTagOffset(int dirStartOffset, int entryNumber)587 {588 // add 2 bytes for the tag count589 // each entry is 12 bytes, so we skip 12 * the number seen so far590 return dirStartOffset + 2 + (12 * entryNumber);591 }592 116 } -
trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata.exif; … … 32 32 import java.util.Map; 33 33 34 import static com.drew.metadata.exif.ExifSubIFDDirectory.*; 35 34 36 /** 35 * Provides human-readable string representations of tag values stored in a <code>ExifSubIFDDirectory</code>.37 * Provides human-readable string representations of tag values stored in a {@link ExifSubIFDDirectory}. 36 38 * 37 * @author Drew Noakes http://drewnoakes.com 39 * @author Drew Noakes https://drewnoakes.com 38 40 */ 39 41 public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory> … … 60 62 61 63 /** 62 * Returns a descriptive value of the thespecified tag for this image.64 * Returns a descriptive value of the specified tag for this image. 63 65 * Where possible, known values will be substituted here in place of the raw 64 66 * tokens actually kept in the Exif segment. If no substitution is 65 67 * available, the value provided by getString(int) will be returned. 68 * 66 69 * @param tagType the tag to find a description for 67 70 * @return a description of the image's value for the specified tag, or 68 71 * <code>null</code> if the tag hasn't been defined. 69 72 */ 73 @Override 70 74 @Nullable 71 75 public String getDescription(int tagType) 72 76 { 73 77 switch (tagType) { 74 case ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE:78 case TAG_NEW_SUBFILE_TYPE: 75 79 return getNewSubfileTypeDescription(); 76 case ExifSubIFDDirectory.TAG_SUBFILE_TYPE:80 case TAG_SUBFILE_TYPE: 77 81 return getSubfileTypeDescription(); 78 case ExifSubIFDDirectory.TAG_THRESHOLDING:82 case TAG_THRESHOLDING: 79 83 return getThresholdingDescription(); 80 case ExifSubIFDDirectory.TAG_FILL_ORDER:84 case TAG_FILL_ORDER: 81 85 return getFillOrderDescription(); 82 case ExifSubIFDDirectory.TAG_EXPOSURE_TIME:86 case TAG_EXPOSURE_TIME: 83 87 return getExposureTimeDescription(); 84 case ExifSubIFDDirectory.TAG_SHUTTER_SPEED:88 case TAG_SHUTTER_SPEED: 85 89 return getShutterSpeedDescription(); 86 case ExifSubIFDDirectory.TAG_FNUMBER:90 case TAG_FNUMBER: 87 91 return getFNumberDescription(); 88 case ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:92 case TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL: 89 93 return getCompressedAverageBitsPerPixelDescription(); 90 case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE:94 case TAG_SUBJECT_DISTANCE: 91 95 return getSubjectDistanceDescription(); 92 case ExifSubIFDDirectory.TAG_METERING_MODE:96 case TAG_METERING_MODE: 93 97 return getMeteringModeDescription(); 94 case ExifSubIFDDirectory.TAG_WHITE_BALANCE:98 case TAG_WHITE_BALANCE: 95 99 return getWhiteBalanceDescription(); 96 case ExifSubIFDDirectory.TAG_FLASH:100 case TAG_FLASH: 97 101 return getFlashDescription(); 98 case ExifSubIFDDirectory.TAG_FOCAL_LENGTH:102 case TAG_FOCAL_LENGTH: 99 103 return getFocalLengthDescription(); 100 case ExifSubIFDDirectory.TAG_COLOR_SPACE:104 case TAG_COLOR_SPACE: 101 105 return getColorSpaceDescription(); 102 case ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH:106 case TAG_EXIF_IMAGE_WIDTH: 103 107 return getExifImageWidthDescription(); 104 case ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT:108 case TAG_EXIF_IMAGE_HEIGHT: 105 109 return getExifImageHeightDescription(); 106 case ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT:110 case TAG_FOCAL_PLANE_RESOLUTION_UNIT: 107 111 return getFocalPlaneResolutionUnitDescription(); 108 case ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES:112 case TAG_FOCAL_PLANE_X_RESOLUTION: 109 113 return getFocalPlaneXResolutionDescription(); 110 case ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES:114 case TAG_FOCAL_PLANE_Y_RESOLUTION: 111 115 return getFocalPlaneYResolutionDescription(); 112 case ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE:116 case TAG_BITS_PER_SAMPLE: 113 117 return getBitsPerSampleDescription(); 114 case ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION:118 case TAG_PHOTOMETRIC_INTERPRETATION: 115 119 return getPhotometricInterpretationDescription(); 116 case ExifSubIFDDirectory.TAG_ROWS_PER_STRIP:120 case TAG_ROWS_PER_STRIP: 117 121 return getRowsPerStripDescription(); 118 case ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS:122 case TAG_STRIP_BYTE_COUNTS: 119 123 return getStripByteCountsDescription(); 120 case ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL:124 case TAG_SAMPLES_PER_PIXEL: 121 125 return getSamplesPerPixelDescription(); 122 case ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION:126 case TAG_PLANAR_CONFIGURATION: 123 127 return getPlanarConfigurationDescription(); 124 case ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING:128 case TAG_YCBCR_SUBSAMPLING: 125 129 return getYCbCrSubsamplingDescription(); 126 case ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM:130 case TAG_EXPOSURE_PROGRAM: 127 131 return getExposureProgramDescription(); 128 case ExifSubIFDDirectory.TAG_APERTURE:132 case TAG_APERTURE: 129 133 return getApertureValueDescription(); 130 case ExifSubIFDDirectory.TAG_MAX_APERTURE:134 case TAG_MAX_APERTURE: 131 135 return getMaxApertureValueDescription(); 132 case ExifSubIFDDirectory.TAG_SENSING_METHOD:136 case TAG_SENSING_METHOD: 133 137 return getSensingMethodDescription(); 134 case ExifSubIFDDirectory.TAG_EXPOSURE_BIAS:138 case TAG_EXPOSURE_BIAS: 135 139 return getExposureBiasDescription(); 136 case ExifSubIFDDirectory.TAG_FILE_SOURCE:140 case TAG_FILE_SOURCE: 137 141 return getFileSourceDescription(); 138 case ExifSubIFDDirectory.TAG_SCENE_TYPE:142 case TAG_SCENE_TYPE: 139 143 return getSceneTypeDescription(); 140 case ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION:144 case TAG_COMPONENTS_CONFIGURATION: 141 145 return getComponentConfigurationDescription(); 142 case ExifSubIFDDirectory.TAG_EXIF_VERSION:146 case TAG_EXIF_VERSION: 143 147 return getExifVersionDescription(); 144 case ExifSubIFDDirectory.TAG_FLASHPIX_VERSION:148 case TAG_FLASHPIX_VERSION: 145 149 return getFlashPixVersionDescription(); 146 case ExifSubIFDDirectory.TAG_ISO_EQUIVALENT:150 case TAG_ISO_EQUIVALENT: 147 151 return getIsoEquivalentDescription(); 148 case ExifSubIFDDirectory.TAG_USER_COMMENT:152 case TAG_USER_COMMENT: 149 153 return getUserCommentDescription(); 150 case ExifSubIFDDirectory.TAG_CUSTOM_RENDERED:154 case TAG_CUSTOM_RENDERED: 151 155 return getCustomRenderedDescription(); 152 case ExifSubIFDDirectory.TAG_EXPOSURE_MODE:156 case TAG_EXPOSURE_MODE: 153 157 return getExposureModeDescription(); 154 case ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE:158 case TAG_WHITE_BALANCE_MODE: 155 159 return getWhiteBalanceModeDescription(); 156 case ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO:160 case TAG_DIGITAL_ZOOM_RATIO: 157 161 return getDigitalZoomRatioDescription(); 158 case ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:162 case TAG_35MM_FILM_EQUIV_FOCAL_LENGTH: 159 163 return get35mmFilmEquivFocalLengthDescription(); 160 case ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE:164 case TAG_SCENE_CAPTURE_TYPE: 161 165 return getSceneCaptureTypeDescription(); 162 case ExifSubIFDDirectory.TAG_GAIN_CONTROL:166 case TAG_GAIN_CONTROL: 163 167 return getGainControlDescription(); 164 case ExifSubIFDDirectory.TAG_CONTRAST:168 case TAG_CONTRAST: 165 169 return getContrastDescription(); 166 case ExifSubIFDDirectory.TAG_SATURATION:170 case TAG_SATURATION: 167 171 return getSaturationDescription(); 168 case ExifSubIFDDirectory.TAG_SHARPNESS:172 case TAG_SHARPNESS: 169 173 return getSharpnessDescription(); 170 case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE:174 case TAG_SUBJECT_DISTANCE_RANGE: 171 175 return getSubjectDistanceRangeDescription(); 172 176 default: … … 178 182 public String getNewSubfileTypeDescription() 179 183 { 180 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE); 181 if (value==null) 182 return null; 183 switch (value) { 184 case 1: return "Full-resolution image"; 185 case 2: return "Reduced-resolution image"; 186 case 3: return "Single page of multi-page reduced-resolution image"; 187 case 4: return "Transparency mask"; 188 case 5: return "Transparency mask of reduced-resolution image"; 189 case 6: return "Transparency mask of multi-page image"; 190 case 7: return "Transparency mask of reduced-resolution multi-page image"; 191 default: 192 return "Unknown (" + value + ")"; 193 } 184 return getIndexedDescription(TAG_NEW_SUBFILE_TYPE, 1, 185 "Full-resolution image", 186 "Reduced-resolution image", 187 "Single page of multi-page reduced-resolution image", 188 "Transparency mask", 189 "Transparency mask of reduced-resolution image", 190 "Transparency mask of multi-page image", 191 "Transparency mask of reduced-resolution multi-page image" 192 ); 194 193 } 195 194 … … 197 196 public String getSubfileTypeDescription() 198 197 { 199 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBFILE_TYPE); 200 if (value==null) 201 return null; 202 switch (value) { 203 case 1: return "Full-resolution image"; 204 case 2: return "Reduced-resolution image"; 205 case 3: return "Single page of multi-page image"; 206 default: 207 return "Unknown (" + value + ")"; 208 } 198 return getIndexedDescription(TAG_SUBFILE_TYPE, 1, 199 "Full-resolution image", 200 "Reduced-resolution image", 201 "Single page of multi-page image" 202 ); 209 203 } 210 204 … … 212 206 public String getThresholdingDescription() 213 207 { 214 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_THRESHOLDING); 215 if (value==null) 216 return null; 217 switch (value) { 218 case 1: return "No dithering or halftoning"; 219 case 2: return "Ordered dither or halftone"; 220 case 3: return "Randomized dither"; 221 default: 222 return "Unknown (" + value + ")"; 223 } 208 return getIndexedDescription(TAG_THRESHOLDING, 1, 209 "No dithering or halftoning", 210 "Ordered dither or halftone", 211 "Randomized dither" 212 ); 224 213 } 225 214 … … 227 216 public String getFillOrderDescription() 228 217 { 229 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FILL_ORDER); 230 if (value==null) 231 return null; 232 switch (value) { 233 case 1: return "Normal"; 234 case 2: return "Reversed"; 235 default: 236 return "Unknown (" + value + ")"; 237 } 218 return getIndexedDescription(TAG_FILL_ORDER, 1, 219 "Normal", 220 "Reversed" 221 ); 238 222 } 239 223 … … 241 225 public String getSubjectDistanceRangeDescription() 242 226 { 243 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE); 244 if (value==null) 245 return null; 246 switch (value) { 247 case 0: return "Unknown"; 248 case 1: return "Macro"; 249 case 2: return "Close view"; 250 case 3: return "Distant view"; 251 default: 252 return "Unknown (" + value + ")"; 253 } 227 return getIndexedDescription(TAG_SUBJECT_DISTANCE_RANGE, 228 "Unknown", 229 "Macro", 230 "Close view", 231 "Distant view" 232 ); 254 233 } 255 234 … … 257 236 public String getSharpnessDescription() 258 237 { 259 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SHARPNESS); 260 if (value==null) 261 return null; 262 switch (value) { 263 case 0: return "None"; 264 case 1: return "Low"; 265 case 2: return "Hard"; 266 default: 267 return "Unknown (" + value + ")"; 268 } 238 return getIndexedDescription(TAG_SHARPNESS, 239 "None", 240 "Low", 241 "Hard" 242 ); 269 243 } 270 244 … … 272 246 public String getSaturationDescription() 273 247 { 274 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SATURATION); 275 if (value==null) 276 return null; 277 switch (value) { 278 case 0: return "None"; 279 case 1: return "Low saturation"; 280 case 2: return "High saturation"; 281 default: 282 return "Unknown (" + value + ")"; 283 } 248 return getIndexedDescription(TAG_SATURATION, 249 "None", 250 "Low saturation", 251 "High saturation" 252 ); 284 253 } 285 254 … … 287 256 public String getContrastDescription() 288 257 { 289 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CONTRAST); 290 if (value==null) 291 return null; 292 switch (value) { 293 case 0: return "None"; 294 case 1: return "Soft"; 295 case 2: return "Hard"; 296 default: 297 return "Unknown (" + value + ")"; 298 } 258 return getIndexedDescription(TAG_CONTRAST, 259 "None", 260 "Soft", 261 "Hard" 262 ); 299 263 } 300 264 … … 302 266 public String getGainControlDescription() 303 267 { 304 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_GAIN_CONTROL); 305 if (value==null) 306 return null; 307 switch (value) { 308 case 0: return "None"; 309 case 1: return "Low gain up"; 310 case 2: return "Low gain down"; 311 case 3: return "High gain up"; 312 case 4: return "High gain down"; 313 default: 314 return "Unknown (" + value + ")"; 315 } 268 return getIndexedDescription(TAG_GAIN_CONTROL, 269 "None", 270 "Low gain up", 271 "Low gain down", 272 "High gain up", 273 "High gain down" 274 ); 316 275 } 317 276 … … 319 278 public String getSceneCaptureTypeDescription() 320 279 { 321 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE); 322 if (value==null) 323 return null; 324 switch (value) { 325 case 0: return "Standard"; 326 case 1: return "Landscape"; 327 case 2: return "Portrait"; 328 case 3: return "Night scene"; 329 default: 330 return "Unknown (" + value + ")"; 331 } 280 return getIndexedDescription(TAG_SCENE_CAPTURE_TYPE, 281 "Standard", 282 "Landscape", 283 "Portrait", 284 "Night scene" 285 ); 332 286 } 333 287 … … 335 289 public String get35mmFilmEquivFocalLengthDescription() 336 290 { 337 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH); 338 if (value==null) 339 return null; 340 if (value==0) 341 return "Unknown"; 342 else 343 return SimpleDecimalFormatter.format(value) + "mm"; 291 Integer value = _directory.getInteger(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH); 292 return value == null 293 ? null 294 : value == 0 295 ? "Unknown" 296 : SimpleDecimalFormatter.format(value) + "mm"; 344 297 } 345 298 … … 347 300 public String getDigitalZoomRatioDescription() 348 301 { 349 Rational value = _directory.getRational( ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO);350 if (value==null)351 return null;352 if (value.getNumerator()==0)353 return"Digital zoom not used.";354 returnSimpleDecimalFormatter.format(value.doubleValue());302 Rational value = _directory.getRational(TAG_DIGITAL_ZOOM_RATIO); 303 return value == null 304 ? null 305 : value.getNumerator() == 0 306 ? "Digital zoom not used." 307 : SimpleDecimalFormatter.format(value.doubleValue()); 355 308 } 356 309 … … 358 311 public String getWhiteBalanceModeDescription() 359 312 { 360 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE); 361 if (value==null) 362 return null; 363 switch (value) { 364 case 0: return "Auto white balance"; 365 case 1: return "Manual white balance"; 366 default: 367 return "Unknown (" + value + ")"; 368 } 313 return getIndexedDescription(TAG_WHITE_BALANCE_MODE, 314 "Auto white balance", 315 "Manual white balance" 316 ); 369 317 } 370 318 … … 372 320 public String getExposureModeDescription() 373 321 { 374 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_MODE); 375 if (value==null) 376 return null; 377 switch (value) { 378 case 0: return "Auto exposure"; 379 case 1: return "Manual exposure"; 380 case 2: return "Auto bracket"; 381 default: 382 return "Unknown (" + value + ")"; 383 } 322 return getIndexedDescription(TAG_EXPOSURE_MODE, 323 "Auto exposure", 324 "Manual exposure", 325 "Auto bracket" 326 ); 384 327 } 385 328 … … 387 330 public String getCustomRenderedDescription() 388 331 { 389 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CUSTOM_RENDERED); 390 if (value==null) 391 return null; 392 switch (value) { 393 case 0: return "Normal process"; 394 case 1: return "Custom process"; 395 default: 396 return "Unknown (" + value + ")"; 397 } 332 return getIndexedDescription(TAG_CUSTOM_RENDERED, 333 "Normal process", 334 "Custom process" 335 ); 398 336 } 399 337 … … 401 339 public String getUserCommentDescription() 402 340 { 403 byte[] commentBytes = _directory.getByteArray( ExifSubIFDDirectory.TAG_USER_COMMENT);404 if (commentBytes ==null)341 byte[] commentBytes = _directory.getByteArray(TAG_USER_COMMENT); 342 if (commentBytes == null) 405 343 return null; 406 344 if (commentBytes.length == 0) … … 408 346 409 347 final Map<String, String> encodingMap = new HashMap<String, String>(); 410 encodingMap.put("ASCII", 411 encodingMap.put("UNICODE", 412 encodingMap.put("JIS", 348 encodingMap.put("ASCII", System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1". 349 encodingMap.put("UNICODE", "UTF-16LE"); 350 encodingMap.put("JIS", "Shift-JIS"); // We assume this charset for now. Another suggestion is "JIS". 413 351 414 352 try { … … 442 380 { 443 381 // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values 444 Integer isoEquiv = _directory.getInteger(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT); 445 if (isoEquiv==null) 446 return null; 382 Integer isoEquiv = _directory.getInteger(TAG_ISO_EQUIVALENT); 447 383 // There used to be a check here that multiplied ISO values < 50 by 200. 448 384 // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40. 449 return Integer.toString(isoEquiv); 385 return isoEquiv != null 386 ? Integer.toString(isoEquiv) 387 : null; 450 388 } 451 389 … … 453 391 public String getExifVersionDescription() 454 392 { 455 int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_EXIF_VERSION); 456 if (ints==null) 457 return null; 458 return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2); 393 return getVersionBytesDescription(TAG_EXIF_VERSION, 2); 459 394 } 460 395 … … 462 397 public String getFlashPixVersionDescription() 463 398 { 464 int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_FLASHPIX_VERSION); 465 if (ints==null) 466 return null; 467 return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2); 399 return getVersionBytesDescription(TAG_FLASHPIX_VERSION, 2); 468 400 } 469 401 … … 471 403 public String getSceneTypeDescription() 472 404 { 473 Integer sceneType = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_TYPE); 474 if (sceneType==null) 475 return null; 476 return sceneType == 1 477 ? "Directly photographed image" 478 : "Unknown (" + sceneType + ")"; 405 return getIndexedDescription(TAG_SCENE_TYPE, 406 1, 407 "Directly photographed image" 408 ); 479 409 } 480 410 … … 482 412 public String getFileSourceDescription() 483 413 { 484 Integer fileSource = _directory.getInteger(ExifSubIFDDirectory.TAG_FILE_SOURCE);485 if (fileSource==null)486 return null;487 return fileSource == 3488 ?"Digital Still Camera (DSC)"489 : "Unknown (" + fileSource + ")";414 return getIndexedDescription(TAG_FILE_SOURCE, 415 1, 416 "Film Scanner", 417 "Reflection Print Scanner", 418 "Digital Still Camera (DSC)" 419 ); 490 420 } 491 421 … … 493 423 public String getExposureBiasDescription() 494 424 { 495 Rational value = _directory.getRational( ExifSubIFDDirectory.TAG_EXPOSURE_BIAS);496 if (value ==null)425 Rational value = _directory.getRational(TAG_EXPOSURE_BIAS); 426 if (value == null) 497 427 return null; 498 428 return value.toSimpleString(true) + " EV"; … … 502 432 public String getMaxApertureValueDescription() 503 433 { 504 Double aperture = _directory.getDoubleObject( ExifSubIFDDirectory.TAG_MAX_APERTURE);505 if (aperture ==null)434 Double aperture = _directory.getDoubleObject(TAG_MAX_APERTURE); 435 if (aperture == null) 506 436 return null; 507 437 double fStop = PhotographicConversions.apertureToFStop(aperture); … … 512 442 public String getApertureValueDescription() 513 443 { 514 Double aperture = _directory.getDoubleObject( ExifSubIFDDirectory.TAG_APERTURE);515 if (aperture ==null)444 Double aperture = _directory.getDoubleObject(TAG_APERTURE); 445 if (aperture == null) 516 446 return null; 517 447 double fStop = PhotographicConversions.apertureToFStop(aperture); … … 522 452 public String getExposureProgramDescription() 523 453 { 524 // '1' means manual control, '2' program normal, '3' aperture priority, 525 // '4' shutter priority, '5' program creative (slow program), 526 // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode. 527 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM); 528 if (value==null) 529 return null; 530 switch (value) { 531 case 1: return "Manual control"; 532 case 2: return "Program normal"; 533 case 3: return "Aperture priority"; 534 case 4: return "Shutter priority"; 535 case 5: return "Program creative (slow program)"; 536 case 6: return "Program action (high-speed program)"; 537 case 7: return "Portrait mode"; 538 case 8: return "Landscape mode"; 539 default: 540 return "Unknown program (" + value + ")"; 541 } 454 return getIndexedDescription(TAG_EXPOSURE_PROGRAM, 455 1, 456 "Manual control", 457 "Program normal", 458 "Aperture priority", 459 "Shutter priority", 460 "Program creative (slow program)", 461 "Program action (high-speed program)", 462 "Portrait mode", 463 "Landscape mode" 464 ); 542 465 } 543 466 … … 545 468 public String getYCbCrSubsamplingDescription() 546 469 { 547 int[] positions = _directory.getIntArray( ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING);548 if (positions ==null)470 int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING); 471 if (positions == null) 549 472 return null; 550 473 if (positions[0] == 2 && positions[1] == 1) { … … 564 487 // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr 565 488 // plane format. 566 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION); 567 if (value==null) 568 return null; 569 switch (value) { 570 case 1: return "Chunky (contiguous for each subsampling pixel)"; 571 case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)"; 572 default: 573 return "Unknown configuration"; 574 } 489 return getIndexedDescription(TAG_PLANAR_CONFIGURATION, 490 1, 491 "Chunky (contiguous for each subsampling pixel)", 492 "Separate (Y-plane/Cb-plane/Cr-plane format)" 493 ); 575 494 } 576 495 … … 578 497 public String getSamplesPerPixelDescription() 579 498 { 580 String value = _directory.getString( ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL);581 return value ==null ? null : value + " samples/pixel";499 String value = _directory.getString(TAG_SAMPLES_PER_PIXEL); 500 return value == null ? null : value + " samples/pixel"; 582 501 } 583 502 … … 585 504 public String getRowsPerStripDescription() 586 505 { 587 final String value = _directory.getString( ExifSubIFDDirectory.TAG_ROWS_PER_STRIP);588 return value ==null ? null : value + " rows/strip";506 final String value = _directory.getString(TAG_ROWS_PER_STRIP); 507 return value == null ? null : value + " rows/strip"; 589 508 } 590 509 … … 592 511 public String getStripByteCountsDescription() 593 512 { 594 final String value = _directory.getString( ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);595 return value ==null ? null : value + " bytes";513 final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS); 514 return value == null ? null : value + " bytes"; 596 515 } 597 516 … … 600 519 { 601 520 // Shows the color space of the image data components 602 Integer value = _directory.getInteger( ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);603 if (value ==null)521 Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION); 522 if (value == null) 604 523 return null; 605 524 switch (value) { … … 626 545 public String getBitsPerSampleDescription() 627 546 { 628 String value = _directory.getString( ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE);629 return value ==null ? null : value + " bits/component/pixel";547 String value = _directory.getString(TAG_BITS_PER_SAMPLE); 548 return value == null ? null : value + " bits/component/pixel"; 630 549 } 631 550 … … 633 552 public String getFocalPlaneXResolutionDescription() 634 553 { 635 Rational rational = _directory.getRational( ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES);636 if (rational ==null)554 Rational rational = _directory.getRational(TAG_FOCAL_PLANE_X_RESOLUTION); 555 if (rational == null) 637 556 return null; 638 557 final String unit = getFocalPlaneResolutionUnitDescription(); 639 558 return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) 640 + (unit ==null ? "" : " " + unit.toLowerCase());559 + (unit == null ? "" : " " + unit.toLowerCase()); 641 560 } 642 561 … … 644 563 public String getFocalPlaneYResolutionDescription() 645 564 { 646 Rational rational = _directory.getRational( ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES);647 if (rational ==null)565 Rational rational = _directory.getRational(TAG_FOCAL_PLANE_Y_RESOLUTION); 566 if (rational == null) 648 567 return null; 649 568 final String unit = getFocalPlaneResolutionUnitDescription(); 650 569 return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) 651 + (unit ==null ? "" : " " + unit.toLowerCase());570 + (unit == null ? "" : " " + unit.toLowerCase()); 652 571 } 653 572 … … 655 574 public String getFocalPlaneResolutionUnitDescription() 656 575 { 657 // Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit, 658 // '2' inch, '3' centimeter. 659 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT); 660 if (value==null) 661 return null; 662 switch (value) { 663 case 1: return "(No unit)"; 664 case 2: return "Inches"; 665 case 3: return "cm"; 666 default: 667 return ""; 668 } 576 // Unit of FocalPlaneXResolution/FocalPlaneYResolution. 577 // '1' means no-unit, '2' inch, '3' centimeter. 578 return getIndexedDescription(TAG_FOCAL_PLANE_RESOLUTION_UNIT, 579 1, 580 "(No unit)", 581 "Inches", 582 "cm" 583 ); 669 584 } 670 585 … … 672 587 public String getExifImageWidthDescription() 673 588 { 674 final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH); 675 if (value==null) 676 return null; 677 return value + " pixels"; 589 final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_WIDTH); 590 return value == null ? null : value + " pixels"; 678 591 } 679 592 … … 681 594 public String getExifImageHeightDescription() 682 595 { 683 final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT); 684 if (value==null) 685 return null; 686 return value + " pixels"; 596 final Integer value = _directory.getInteger(TAG_EXIF_IMAGE_HEIGHT); 597 return value == null ? null : value + " pixels"; 687 598 } 688 599 … … 690 601 public String getColorSpaceDescription() 691 602 { 692 final Integer value = _directory.getInteger( ExifSubIFDDirectory.TAG_COLOR_SPACE);693 if (value ==null)603 final Integer value = _directory.getInteger(TAG_COLOR_SPACE); 604 if (value == null) 694 605 return null; 695 606 if (value == 1) … … 697 608 if (value == 65535) 698 609 return "Undefined"; 699 return "Unknown"; 610 return "Unknown (" + value + ")"; 700 611 } 701 612 … … 703 614 public String getFocalLengthDescription() 704 615 { 705 Rational value = _directory.getRational( ExifSubIFDDirectory.TAG_FOCAL_LENGTH);706 if (value ==null)616 Rational value = _directory.getRational(TAG_FOCAL_LENGTH); 617 if (value == null) 707 618 return null; 708 619 java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); … … 714 625 { 715 626 /* 716 * This is a bitmask. 627 * This is a bit mask. 717 628 * 0 = flash fired 718 629 * 1 = return detected … … 724 635 */ 725 636 726 final Integer value = _directory.getInteger( ExifSubIFDDirectory.TAG_FLASH);727 728 if (value ==null)637 final Integer value = _directory.getInteger(TAG_FLASH); 638 639 if (value == null) 729 640 return null; 730 641 731 642 StringBuilder sb = new StringBuilder(); 732 643 733 if ((value & 0x1) !=0)644 if ((value & 0x1) != 0) 734 645 sb.append("Flash fired"); 735 646 else … … 737 648 738 649 // check if we're able to detect a return, before we mention it 739 if ((value & 0x4)!=0) 740 { 741 if ((value & 0x2)!=0) 650 if ((value & 0x4) != 0) { 651 if ((value & 0x2) != 0) 742 652 sb.append(", return detected"); 743 653 else … … 745 655 } 746 656 747 if ((value & 0x10) !=0)657 if ((value & 0x10) != 0) 748 658 sb.append(", auto"); 749 659 750 if ((value & 0x40) !=0)660 if ((value & 0x40) != 0) 751 661 sb.append(", red-eye reduction"); 752 662 … … 760 670 // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55, 761 671 // '21' D65, '22' D75, '255' other. 762 final Integer value = _directory.getInteger( ExifSubIFDDirectory.TAG_WHITE_BALANCE);763 if (value ==null)672 final Integer value = _directory.getInteger(TAG_WHITE_BALANCE); 673 if (value == null) 764 674 return null; 765 675 switch (value) { … … 786 696 // '0' means unknown, '1' average, '2' center weighted average, '3' spot 787 697 // '4' multi-spot, '5' multi-segment, '6' partial, '255' other 788 Integer value = _directory.getInteger( ExifSubIFDDirectory.TAG_METERING_MODE);789 if (value ==null)698 Integer value = _directory.getInteger(TAG_METERING_MODE); 699 if (value == null) 790 700 return null; 791 701 switch (value) { … … 806 716 public String getSubjectDistanceDescription() 807 717 { 808 Rational value = _directory.getRational( ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE);809 if (value ==null)718 Rational value = _directory.getRational(TAG_SUBJECT_DISTANCE); 719 if (value == null) 810 720 return null; 811 721 java.text.DecimalFormat formatter = new DecimalFormat("0.0##"); … … 816 726 public String getCompressedAverageBitsPerPixelDescription() 817 727 { 818 Rational value = _directory.getRational( ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);819 if (value ==null)728 Rational value = _directory.getRational(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL); 729 if (value == null) 820 730 return null; 821 731 String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals); 822 if (value.isInteger() && value.intValue() == 1) { 823 return ratio + " bit/pixel"; 824 } else { 825 return ratio + " bits/pixel"; 826 } 732 return value.isInteger() && value.intValue() == 1 733 ? ratio + " bit/pixel" 734 : ratio + " bits/pixel"; 827 735 } 828 736 … … 830 738 public String getExposureTimeDescription() 831 739 { 832 String value = _directory.getString( ExifSubIFDDirectory.TAG_EXPOSURE_TIME);833 return value ==null ? null : value + " sec";740 String value = _directory.getString(TAG_EXPOSURE_TIME); 741 return value == null ? null : value + " sec"; 834 742 } 835 743 … … 847 755 // description (spotted bug using a Canon EOS 300D) 848 756 // thanks also to Gli Blr for spotting this bug 849 Float apexValue = _directory.getFloatObject( ExifSubIFDDirectory.TAG_SHUTTER_SPEED);850 if (apexValue ==null)851 return null; 852 if (apexValue <=1) {853 float apexPower = (float)(1 /(Math.exp(apexValue*Math.log(2))));757 Float apexValue = _directory.getFloatObject(TAG_SHUTTER_SPEED); 758 if (apexValue == null) 759 return null; 760 if (apexValue <= 1) { 761 float apexPower = (float)(1 / (Math.exp(apexValue * Math.log(2)))); 854 762 long apexPower10 = Math.round((double)apexPower * 10.0); 855 float fApexPower = (float) 763 float fApexPower = (float)apexPower10 / 10.0f; 856 764 return fApexPower + " sec"; 857 765 } else { 858 int apexPower = (int)((Math.exp(apexValue *Math.log(2))));766 int apexPower = (int)((Math.exp(apexValue * Math.log(2)))); 859 767 return "1/" + apexPower + " sec"; 860 768 } … … 879 787 return sb.toString(); 880 788 */ 881 882 789 } 883 790 … … 885 792 public String getFNumberDescription() 886 793 { 887 Rational value = _directory.getRational( ExifSubIFDDirectory.TAG_FNUMBER);888 if (value ==null)794 Rational value = _directory.getRational(TAG_FNUMBER); 795 if (value == null) 889 796 return null; 890 797 return "F" + SimpleDecimalFormatter.format(value.doubleValue()); … … 897 804 // '4' Three-chip color area sensor, '5' Color sequential area sensor 898 805 // '7' Trilinear sensor '8' Color sequential linear sensor, 'Other' reserved 899 Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SENSING_METHOD); 900 if (value==null) 901 return null; 902 switch (value) { 903 case 1: return "(Not defined)"; 904 case 2: return "One-chip color area sensor"; 905 case 3: return "Two-chip color area sensor"; 906 case 4: return "Three-chip color area sensor"; 907 case 5: return "Color sequential area sensor"; 908 case 7: return "Trilinear sensor"; 909 case 8: return "Color sequential linear sensor"; 910 default: 911 return ""; 912 } 806 return getIndexedDescription(TAG_SENSING_METHOD, 807 1, 808 "(Not defined)", 809 "One-chip color area sensor", 810 "Two-chip color area sensor", 811 "Three-chip color area sensor", 812 "Color sequential area sensor", 813 null, 814 "Trilinear sensor", 815 "Color sequential linear sensor" 816 ); 913 817 } 914 818 … … 916 820 public String getComponentConfigurationDescription() 917 821 { 918 int[] components = _directory.getIntArray( ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION);919 if (components ==null)822 int[] components = _directory.getIntArray(TAG_COMPONENTS_CONFIGURATION); 823 if (components == null) 920 824 return null; 921 825 String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"}; -
trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 package com.drew.metadata.exif; … … 29 29 * Describes Exif tags from the SubIFD directory. 30 30 * 31 * @author Drew Noakes http://drewnoakes.com 31 * @author Drew Noakes https://drewnoakes.com 32 32 */ 33 33 public class ExifSubIFDDirectory extends Directory … … 133 133 /** 134 134 * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524. 135 * <p />135 * <p> 136 136 * OECF is the relationship between the camera optical input and the image values. 137 * <p />137 * <p> 138 138 * The values are: 139 139 * <ul> … … 260 260 */ 261 261 public static final int TAG_FOCAL_LENGTH = 0x920A; 262 263 /** 264 * This tag holds the Exif Makernote. Makernotes are free to be in any format, though they are often IFDs. 265 * To determine the format, we consider the starting bytes of the makernote itself and sometimes the 266 * camera model and make. 267 * <p> 268 * The component count for this tag includes all of the bytes needed for the makernote. 269 */ 270 public static final int TAG_MAKERNOTE = 0x927C; 271 262 272 public static final int TAG_USER_COMMENT = 0x9286; 273 263 274 public static final int TAG_SUBSECOND_TIME = 0x9290; 264 275 public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291; 265 276 public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292; 277 266 278 public static final int TAG_FLASHPIX_VERSION = 0xA000; 267 279 /** … … 274 286 public static final int TAG_EXIF_IMAGE_HEIGHT = 0xA003; 275 287 public static final int TAG_RELATED_SOUND_FILE = 0xA004; 276 public static final int TAG_FOCAL_PLANE_X_RES = 0xA20E; 277 public static final int TAG_FOCAL_PLANE_Y_RES = 0xA20F; 288 289 /** This tag is a pointer to the Exif Interop IFD. */ 290 public static final int TAG_INTEROP_OFFSET = 0xA005; 291 292 public static final int TAG_FOCAL_PLANE_X_RESOLUTION = 0xA20E; 293 public static final int TAG_FOCAL_PLANE_Y_RESOLUTION = 0xA20F; 278 294 /** 279 295 * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit, … … 285 301 * been changed to use value '2' but it doesn't match to actual value also. 286 302 */ 287 public static final int TAG_FOCAL_PLANE_UNIT = 0xA210; 303 public static final int TAG_FOCAL_PLANE_RESOLUTION_UNIT = 0xA210; 288 304 public static final int TAG_EXPOSURE_INDEX = 0xA215; 289 305 public static final int TAG_SENSING_METHOD = 0xA217; … … 512 528 _tagNameMap.put(0x0200, "JPEG Proc"); 513 529 _tagNameMap.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel"); 514 _tagNameMap.put( 0x927C, "MakerNote");515 _tagNameMap.put( 0xA005, "Interoperability Offset");530 _tagNameMap.put(TAG_MAKERNOTE, "Makernote"); 531 _tagNameMap.put(TAG_INTEROP_OFFSET, "Interoperability Offset"); 516 532 517 533 _tagNameMap.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type"); … … 586 602 _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE_2, "Spatial Frequency Response"); 587 603 // 0x920E in TIFF/EP 588 _tagNameMap.put(TAG_FOCAL_PLANE_X_RES, "Focal Plane X Resolution"); 604 _tagNameMap.put(TAG_FOCAL_PLANE_X_RESOLUTION, "Focal Plane X Resolution"); 589 605 // 0x920F in TIFF/EP 590 _tagNameMap.put(TAG_FOCAL_PLANE_Y_RES, "Focal Plane Y Resolution"); 606 _tagNameMap.put(TAG_FOCAL_PLANE_Y_RESOLUTION, "Focal Plane Y Resolution"); 591 607 // 0x9210 in TIFF/EP 592 _tagNameMap.put(TAG_FOCAL_PLANE_UNIT, "Focal Plane Resolution Unit"); 608 _tagNameMap.put(TAG_FOCAL_PLANE_RESOLUTION_UNIT, "Focal Plane Resolution Unit"); 593 609 // 0x9214 in TIFF/EP 594 610 _tagNameMap.put(TAG_SUBJECT_LOCATION_2, "Subject Location"); … … 614 630 _tagNameMap.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range"); 615 631 _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID"); 616 632 617 633 _tagNameMap.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name"); 618 634 _tagNameMap.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number"); … … 634 650 } 635 651 652 @Override 636 653 @NotNull 637 654 public String getName() … … 640 657 } 641 658 659 @Override 642 660 @NotNull 643 661 protected HashMap<Integer, String> getTagNameMap() -
trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 27 27 import com.drew.metadata.TagDescriptor; 28 28 29 import static com.drew.metadata.exif.ExifThumbnailDirectory.*; 30 29 31 /** 30 * Provides human-readable string representations of tag values stored in a <code>ExifThumbnailDirectory</code>.31 * 32 * @author Drew Noakes http://drewnoakes.com 32 * Provides human-readable string representations of tag values stored in a {@link ExifThumbnailDirectory}. 33 * 34 * @author Drew Noakes https://drewnoakes.com 33 35 */ 34 36 public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirectory> … … 52 54 53 55 /** 54 * Returns a descriptive value of the thespecified tag for this image.56 * Returns a descriptive value of the specified tag for this image. 55 57 * Where possible, known values will be substituted here in place of the raw 56 58 * tokens actually kept in the Exif segment. If no substitution is 57 59 * available, the value provided by getString(int) will be returned. 60 * 58 61 * @param tagType the tag to find a description for 59 62 * @return a description of the image's value for the specified tag, or 60 63 * <code>null</code> if the tag hasn't been defined. 61 64 */ 65 @Override 62 66 @Nullable 63 67 public String getDescription(int tagType) 64 68 { 65 69 switch (tagType) { 66 case ExifThumbnailDirectory.TAG_ORIENTATION:70 case TAG_ORIENTATION: 67 71 return getOrientationDescription(); 68 case ExifThumbnailDirectory.TAG_RESOLUTION_UNIT:72 case TAG_RESOLUTION_UNIT: 69 73 return getResolutionDescription(); 70 case ExifThumbnailDirectory.TAG_YCBCR_POSITIONING:74 case TAG_YCBCR_POSITIONING: 71 75 return getYCbCrPositioningDescription(); 72 case ExifThumbnailDirectory.TAG_X_RESOLUTION:76 case TAG_X_RESOLUTION: 73 77 return getXResolutionDescription(); 74 case ExifThumbnailDirectory.TAG_Y_RESOLUTION:78 case TAG_Y_RESOLUTION: 75 79 return getYResolutionDescription(); 76 case ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET:80 case TAG_THUMBNAIL_OFFSET: 77 81 return getThumbnailOffsetDescription(); 78 case ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH:82 case TAG_THUMBNAIL_LENGTH: 79 83 return getThumbnailLengthDescription(); 80 case ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH:84 case TAG_THUMBNAIL_IMAGE_WIDTH: 81 85 return getThumbnailImageWidthDescription(); 82 case ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT:86 case TAG_THUMBNAIL_IMAGE_HEIGHT: 83 87 return getThumbnailImageHeightDescription(); 84 case ExifThumbnailDirectory.TAG_BITS_PER_SAMPLE:88 case TAG_BITS_PER_SAMPLE: 85 89 return getBitsPerSampleDescription(); 86 case ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION:90 case TAG_THUMBNAIL_COMPRESSION: 87 91 return getCompressionDescription(); 88 case ExifThumbnailDirectory.TAG_PHOTOMETRIC_INTERPRETATION:92 case TAG_PHOTOMETRIC_INTERPRETATION: 89 93 return getPhotometricInterpretationDescription(); 90 case ExifThumbnailDirectory.TAG_ROWS_PER_STRIP:94 case TAG_ROWS_PER_STRIP: 91 95 return getRowsPerStripDescription(); 92 case ExifThumbnailDirectory.TAG_STRIP_BYTE_COUNTS:96 case TAG_STRIP_BYTE_COUNTS: 93 97 return getStripByteCountsDescription(); 94 case ExifThumbnailDirectory.TAG_SAMPLES_PER_PIXEL:98 case TAG_SAMPLES_PER_PIXEL: 95 99 return getSamplesPerPixelDescription(); 96 case ExifThumbnailDirectory.TAG_PLANAR_CONFIGURATION:100 case TAG_PLANAR_CONFIGURATION: 97 101 return getPlanarConfigurationDescription(); 98 case ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING:102 case TAG_YCBCR_SUBSAMPLING: 99 103 return getYCbCrSubsamplingDescription(); 100 case ExifThumbnailDirectory.TAG_REFERENCE_BLACK_WHITE:104 case TAG_REFERENCE_BLACK_WHITE: 101 105 return getReferenceBlackWhiteDescription(); 102 106 default: … … 108 112 public String getReferenceBlackWhiteDescription() 109 113 { 110 int[] ints = _directory.getIntArray( ExifThumbnailDirectory.TAG_REFERENCE_BLACK_WHITE);111 if (ints ==null)114 int[] ints = _directory.getIntArray(TAG_REFERENCE_BLACK_WHITE); 115 if (ints == null || ints.length < 6) 112 116 return null; 113 117 int blackR = ints[0]; … … 117 121 int blackB = ints[4]; 118 122 int whiteB = ints[5]; 119 return "[" + blackR + "," + blackG + "," + blackB + "] " + 120 "[" + whiteR + "," + whiteG + "," + whiteB + "]"; 123 return String.format("[%d,%d,%d] [%d,%d,%d]", blackR, blackG, blackB, whiteR, whiteG, whiteB); 121 124 } 122 125 … … 124 127 public String getYCbCrSubsamplingDescription() 125 128 { 126 int[] positions = _directory.getIntArray( ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING);127 if (positions ==null || positions.length < 2)129 int[] positions = _directory.getIntArray(TAG_YCBCR_SUBSAMPLING); 130 if (positions == null || positions.length < 2) 128 131 return null; 129 132 if (positions[0] == 2 && positions[1] == 1) { … … 143 146 // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr 144 147 // plane format. 145 Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_PLANAR_CONFIGURATION); 146 if (value==null) 147 return null; 148 switch (value) { 149 case 1: return "Chunky (contiguous for each subsampling pixel)"; 150 case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)"; 151 default: 152 return "Unknown configuration"; 153 } 148 return getIndexedDescription(TAG_PLANAR_CONFIGURATION, 149 1, 150 "Chunky (contiguous for each subsampling pixel)", 151 "Separate (Y-plane/Cb-plane/Cr-plane format)" 152 ); 154 153 } 155 154 … … 157 156 public String getSamplesPerPixelDescription() 158 157 { 159 String value = _directory.getString( ExifThumbnailDirectory.TAG_SAMPLES_PER_PIXEL);160 return value ==null ? null : value + " samples/pixel";158 String value = _directory.getString(TAG_SAMPLES_PER_PIXEL); 159 return value == null ? null : value + " samples/pixel"; 161 160 } 162 161 … … 164 163 public String getRowsPerStripDescription() 165 164 { 166 final String value = _directory.getString( ExifThumbnailDirectory.TAG_ROWS_PER_STRIP);167 return value ==null ? null : value + " rows/strip";165 final String value = _directory.getString(TAG_ROWS_PER_STRIP); 166 return value == null ? null : value + " rows/strip"; 168 167 } 169 168 … … 171 170 public String getStripByteCountsDescription() 172 171 { 173 final String value = _directory.getString( ExifThumbnailDirectory.TAG_STRIP_BYTE_COUNTS);174 return value ==null ? null : value + " bytes";172 final String value = _directory.getString(TAG_STRIP_BYTE_COUNTS); 173 return value == null ? null : value + " bytes"; 175 174 } 176 175 … … 179 178 { 180 179 // Shows the color space of the image data components 181 Integer value = _directory.getInteger( ExifThumbnailDirectory.TAG_PHOTOMETRIC_INTERPRETATION);182 if (value ==null)180 Integer value = _directory.getInteger(TAG_PHOTOMETRIC_INTERPRETATION); 181 if (value == null) 183 182 return null; 184 183 switch (value) { … … 205 204 public String getCompressionDescription() 206 205 { 207 Integer value = _directory.getInteger( ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION);208 if (value ==null)206 Integer value = _directory.getInteger(TAG_THUMBNAIL_COMPRESSION); 207 if (value == null) 209 208 return null; 210 209 switch (value) { … … 244 243 public String getBitsPerSampleDescription() 245 244 { 246 String value = _directory.getString( ExifThumbnailDirectory.TAG_BITS_PER_SAMPLE);247 return value ==null ? null : value + " bits/component/pixel";245 String value = _directory.getString(TAG_BITS_PER_SAMPLE); 246 return value == null ? null : value + " bits/component/pixel"; 248 247 } 249 248 … … 251 250 public String getThumbnailImageWidthDescription() 252 251 { 253 String value = _directory.getString( ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);254 return value ==null ? null : value + " pixels";252 String value = _directory.getString(TAG_THUMBNAIL_IMAGE_WIDTH); 253 return value == null ? null : value + " pixels"; 255 254 } 256 255 … … 258 257 public String getThumbnailImageHeightDescription() 259 258 { 260 String value = _directory.getString( ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);261 return value ==null ? null : value + " pixels";259 String value = _directory.getString(TAG_THUMBNAIL_IMAGE_HEIGHT); 260 return value == null ? null : value + " pixels"; 262 261 } 263 262 … … 265 264 public String getThumbnailLengthDescription() 266 265 { 267 String value = _directory.getString( ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);268 return value ==null ? null : value + " bytes";266 String value = _directory.getString(TAG_THUMBNAIL_LENGTH); 267 return value == null ? null : value + " bytes"; 269 268 } 270 269 … … 272 271 public String getThumbnailOffsetDescription() 273 272 { 274 String value = _directory.getString( ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);275 return value ==null ? null : value + " bytes";273 String value = _directory.getString(TAG_THUMBNAIL_OFFSET); 274 return value == null ? null : value + " bytes"; 276 275 } 277 276 … … 279 278 public String getYResolutionDescription() 280 279 { 281 Rational value = _directory.getRational( ExifThumbnailDirectory.TAG_Y_RESOLUTION);282 if (value ==null)280 Rational value = _directory.getRational(TAG_Y_RESOLUTION); 281 if (value == null) 283 282 return null; 284 283 final String unit = getResolutionDescription(); 285 284 return value.toSimpleString(_allowDecimalRepresentationOfRationals) + 286 287 (unit==null ? "unit" : unit.toLowerCase());285 " dots per " + 286 (unit == null ? "unit" : unit.toLowerCase()); 288 287 } 289 288 … … 291 290 public String getXResolutionDescription() 292 291 { 293 Rational value = _directory.getRational( ExifThumbnailDirectory.TAG_X_RESOLUTION);294 if (value ==null)292 Rational value = _directory.getRational(TAG_X_RESOLUTION); 293 if (value == null) 295 294 return null; 296 295 final String unit = getResolutionDescription(); 297 296 return value.toSimpleString(_allowDecimalRepresentationOfRationals) + 298 299 (unit==null ? "unit" : unit.toLowerCase());297 " dots per " + 298 (unit == null ? "unit" : unit.toLowerCase()); 300 299 } 301 300 … … 303 302 public String getYCbCrPositioningDescription() 304 303 { 305 Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_YCBCR_POSITIONING); 306 if (value==null) 307 return null; 308 switch (value) { 309 case 1: return "Center of pixel array"; 310 case 2: return "Datum point"; 311 default: 312 return String.valueOf(value); 313 } 304 return getIndexedDescription(TAG_YCBCR_POSITIONING, 1, "Center of pixel array", "Datum point"); 314 305 } 315 306 … … 317 308 public String getOrientationDescription() 318 309 { 319 Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_ORIENTATION); 320 if (value==null) 321 return null; 322 switch (value) { 323 case 1: return "Top, left side (Horizontal / normal)"; 324 case 2: return "Top, right side (Mirror horizontal)"; 325 case 3: return "Bottom, right side (Rotate 180)"; 326 case 4: return "Bottom, left side (Mirror vertical)"; 327 case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)"; 328 case 6: return "Right side, top (Rotate 90 CW)"; 329 case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)"; 330 case 8: return "Left side, bottom (Rotate 270 CW)"; 331 default: 332 return String.valueOf(value); 333 } 310 return getIndexedDescription(TAG_ORIENTATION, 1, 311 "Top, left side (Horizontal / normal)", 312 "Top, right side (Mirror horizontal)", 313 "Bottom, right side (Rotate 180)", 314 "Bottom, left side (Mirror vertical)", 315 "Left side, top (Mirror horizontal and rotate 270 CW)", 316 "Right side, top (Rotate 90 CW)", 317 "Right side, bottom (Mirror horizontal and rotate 90 CW)", 318 "Left side, bottom (Rotate 270 CW)"); 334 319 } 335 320 … … 338 323 { 339 324 // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch) 340 Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_RESOLUTION_UNIT); 341 if (value==null) 342 return null; 343 switch (value) { 344 case 1: return "(No unit)"; 345 case 2: return "Inch"; 346 case 3: return "cm"; 347 default: 348 return ""; 349 } 325 return getIndexedDescription(TAG_RESOLUTION_UNIT, 1, "(No unit)", "Inch", "cm"); 350 326 } 351 327 } -
trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
r6127 r8132 1 1 /* 2 * Copyright 2002-201 2Drew Noakes2 * Copyright 2002-2015 Drew Noakes 3 3 * 4 4 * Licensed under the Apache License, Version 2.0 (the "License"); … … 16 16 * More information about this project is available at: 17 17 * 18 * http://drewnoakes.com/code/exif/ 19 * http ://code.google.com/p/metadata-extractor/18 * https://drewnoakes.com/code/exif/ 19 * https://github.com/drewnoakes/metadata-extractor 20 20 */ 21 21 … … 34 34 * One of several Exif directories. Otherwise known as IFD1, this directory holds information about an embedded thumbnail image. 35 35 * 36 * @author Drew Noakes http://drewnoakes.com 36 * @author Drew Noakes https://drewnoakes.com 37 37 */ 38 38 public class ExifThumbnailDirectory extends Directory … … 57 57 * 7 = JPEG 58 58 * 8 = Adobe Deflate 59 * 9 = JBIG B&W 59 * 9 = JBIG B&W 60 60 * 10 = JBIG Color 61 61 * 32766 = Next … … 98 98 public static final int TAG_PHOTOMETRIC_INTERPRETATI