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.Instant;
009import java.util.EnumMap;
010import java.util.LinkedHashMap;
011import java.util.Map;
012import java.util.Optional;
013import java.util.function.Function;
014import java.util.function.Predicate;
015import java.util.stream.Stream;
016
017import com.drew.imaging.ImageMetadataReader;
018import com.drew.imaging.ImageProcessingException;
019import com.drew.lang.GeoLocation;
020import com.drew.metadata.Directory;
021import com.drew.metadata.Metadata;
022import com.drew.metadata.Tag;
023import com.drew.metadata.exif.ExifIFD0Directory;
024import com.drew.metadata.exif.ExifSubIFDDirectory;
025import com.drew.metadata.exif.GpsDirectory;
026import com.drew.metadata.file.FileSystemDirectory;
027
028import net.filebot.util.ExtensionFileFilter;
029import net.filebot.web.Geocode;
030import net.filebot.web.Geocode.AddressComponent;
031
032public class ImageMetadata {
033
034        private final Metadata metadata;
035
036        public ImageMetadata(File file) throws ImageProcessingException, IOException {
037                if (!SUPPORTED_FILE_TYPES.accept(file)) {
038                        throw new IllegalArgumentException("Image type not supported: " + file);
039                }
040
041                this.metadata = ImageMetadataReader.readMetadata(file);
042        }
043
044        public Map<String, String> snapshot() {
045                return snapshot(Tag::getTagName);
046        }
047
048        public Map<String, String> snapshot(Function<Tag, String> key) {
049                return snapshot(key, d -> Stream.of("JPEG", "JFIF", "Interoperability", "Huffman", "File").noneMatch(d.getName()::equals));
050        }
051
052        public Map<String, String> snapshot(Function<Tag, String> key, Predicate<Directory> accept) {
053                Map<String, String> values = new LinkedHashMap<String, String>();
054
055                for (Directory directory : metadata.getDirectories()) {
056                        if (accept.test(directory)) {
057                                for (Tag tag : directory.getTags()) {
058                                        String v = tag.getDescription();
059                                        if (v != null && v.length() > 0) {
060                                                values.put(key.apply(tag), v);
061                                        }
062                                }
063                        }
064                }
065
066                return values;
067        }
068
069        public Optional<String> getName() {
070                return extract(m -> m.getFirstDirectoryOfType(FileSystemDirectory.class)).map(d -> d.getString(FileSystemDirectory.TAG_FILE_NAME));
071        }
072
073        public Optional<Instant> getDateTaken() {
074                return extract(m -> m.getFirstDirectoryOfType(ExifIFD0Directory.class)).map(d -> d.getDate(ExifSubIFDDirectory.TAG_DATETIME)).map(d -> d.toInstant());
075        }
076
077        public Optional<Map<CameraProperty, String>> getCameraModel() {
078                return extract(m -> m.getFirstDirectoryOfType(ExifIFD0Directory.class)).map(d -> {
079                        String maker = d.getDescription(ExifIFD0Directory.TAG_MAKE);
080                        String model = d.getDescription(ExifIFD0Directory.TAG_MODEL);
081
082                        Map<CameraProperty, String> camera = new EnumMap<CameraProperty, String>(CameraProperty.class);
083                        if (maker != null) {
084                                camera.put(CameraProperty.maker, maker);
085                        }
086                        if (model != null) {
087                                camera.put(CameraProperty.model, model);
088                        }
089
090                        return camera;
091                }).filter(m -> !m.isEmpty());
092        }
093
094        public enum CameraProperty {
095                maker, model;
096        }
097
098        public Optional<GeoLocation> getLocationTaken() {
099                return extract(m -> m.getFirstDirectoryOfType(GpsDirectory.class)).map(GpsDirectory::getGeoLocation);
100        }
101
102        public Optional<Map<AddressComponent, String>> getLocationTaken(Geocode geocode) {
103                return getLocationTaken().map(position -> {
104                        try {
105                                return geocode.locate(position.getLatitude(), position.getLongitude());
106                        } catch (Exception e) {
107                                trace(e);
108                        }
109                        return null;
110                });
111        }
112
113        public <T> Optional<T> extract(Function<Metadata, T> extract) {
114                try {
115                        return Optional.ofNullable(extract.apply(metadata));
116                } catch (Exception e) {
117                        debug.finest(cause("Failed to extract image metadata", e));
118                }
119                return Optional.empty();
120        }
121
122        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");
123
124}