001package net.filebot.media; 002 003import static net.filebot.Logging.*; 004 005import java.io.File; 006import java.io.FileFilter; 007import java.io.IOException; 008import java.time.ZoneId; 009import java.time.ZoneOffset; 010import java.time.ZonedDateTime; 011import java.util.Date; 012import java.util.EnumMap; 013import java.util.LinkedHashMap; 014import java.util.Map; 015import java.util.Optional; 016import java.util.function.Function; 017import java.util.function.Predicate; 018import java.util.stream.Stream; 019 020import com.drew.imaging.ImageMetadataReader; 021import com.drew.imaging.ImageProcessingException; 022import com.drew.lang.GeoLocation; 023import com.drew.metadata.Directory; 024import com.drew.metadata.Metadata; 025import com.drew.metadata.Tag; 026import com.drew.metadata.exif.ExifIFD0Directory; 027import com.drew.metadata.exif.ExifSubIFDDirectory; 028import com.drew.metadata.exif.GpsDirectory; 029import com.drew.metadata.file.FileSystemDirectory; 030 031import net.filebot.util.ExtensionFileFilter; 032import net.filebot.web.Geocode; 033import net.filebot.web.Geocode.AddressComponent; 034 035public class ImageMetadata { 036 037 private final Metadata metadata; 038 039 public ImageMetadata(File file) throws ImageProcessingException, IOException { 040 if (!SUPPORTED_FILE_TYPES.accept(file)) { 041 throw new IllegalArgumentException("Image type not supported: " + file); 042 } 043 044 this.metadata = ImageMetadataReader.readMetadata(file); 045 } 046 047 public Map<String, String> snapshot() { 048 return snapshot(Tag::getTagName); 049 } 050 051 public Map<String, String> snapshot(Function<Tag, String> key) { 052 return snapshot(key, d -> Stream.of("JPEG", "JFIF", "Interoperability", "Huffman", "File").noneMatch(d.getName()::equals)); 053 } 054 055 public Map<String, String> snapshot(Function<Tag, String> key, Predicate<Directory> accept) { 056 Map<String, String> values = new LinkedHashMap<String, String>(); 057 058 for (Directory directory : metadata.getDirectories()) { 059 if (accept.test(directory)) { 060 for (Tag tag : directory.getTags()) { 061 String v = tag.getDescription(); 062 if (v != null && v.length() > 0) { 063 values.put(key.apply(tag), v); 064 } 065 } 066 } 067 } 068 069 return values; 070 } 071 072 public Optional<String> getName() { 073 return extract(m -> m.getFirstDirectoryOfType(FileSystemDirectory.class)).map(d -> d.getString(FileSystemDirectory.TAG_FILE_NAME)); 074 } 075 076 public Optional<ZonedDateTime> getDateTaken() { 077 return extract(m -> { 078 // e.g. 079 // Date/Time Original : 2025:05:31 18:01:02 080 // Time Zone Original : +08:00 081 for (ExifSubIFDDirectory subifd : m.getDirectoriesOfType(ExifSubIFDDirectory.class)) { 082 Date date = subifd.getDateOriginal(); 083 if (date != null) { 084 return date.toInstant().atZone(getTimeZone().orElse(ZoneOffset.UTC)); 085 } 086 } 087 return null; 088 }); 089 } 090 091 public Optional<ZoneId> getTimeZone() { 092 return extract(m -> { 093 // e.g. 094 // Date/Time Original : 2025:05:31 18:01:02 095 // Time Zone Original : +08:00 096 for (ExifSubIFDDirectory subifd : m.getDirectoriesOfType(ExifSubIFDDirectory.class)) { 097 String zone = subifd.getString(ExifSubIFDDirectory.TAG_TIME_ZONE_ORIGINAL); 098 if (zone != null) { 099 return ZoneId.of(zone); 100 } 101 } 102 return null; 103 }); 104 } 105 106 public Optional<Map<CameraProperty, String>> getCameraModel() { 107 return extract(m -> m.getFirstDirectoryOfType(ExifIFD0Directory.class)).map(d -> { 108 String maker = d.getDescription(ExifIFD0Directory.TAG_MAKE); 109 String model = d.getDescription(ExifIFD0Directory.TAG_MODEL); 110 111 Map<CameraProperty, String> camera = new EnumMap<CameraProperty, String>(CameraProperty.class); 112 if (maker != null) { 113 camera.put(CameraProperty.maker, maker); 114 } 115 if (model != null) { 116 camera.put(CameraProperty.model, model); 117 } 118 119 return camera; 120 }).filter(m -> !m.isEmpty()); 121 } 122 123 public enum CameraProperty { 124 maker, model; 125 } 126 127 public Optional<GeoLocation> getLocationTaken() { 128 return extract(m -> m.getFirstDirectoryOfType(GpsDirectory.class)).map(GpsDirectory::getGeoLocation); 129 } 130 131 public Optional<Map<AddressComponent, String>> getLocationTaken(Geocode geocode) { 132 return getLocationTaken().map(position -> { 133 try { 134 return geocode.locate(position.getLatitude(), position.getLongitude()); 135 } catch (Exception e) { 136 trace(e); 137 } 138 return null; 139 }); 140 } 141 142 public <T> Optional<T> extract(Function<Metadata, T> extract) { 143 try { 144 return Optional.ofNullable(extract.apply(metadata)); 145 } catch (Exception e) { 146 debug.finest(cause("Failed to extract image metadata", e)); 147 } 148 return Optional.empty(); 149 } 150 151 public static final FileFilter SUPPORTED_FILE_TYPES = new ExtensionFileFilter("jpg", "jpeg", "heic", "png", "webp", "gif", "ico", "bmp", "tif", "tiff", "psd", "pcx", "raw", "crw", "cr2", "nef", "orf", "raf", "rw2", "rwl", "srw", "arw", "dng", "x3f"); 152 153}