feat(gallery): read the all-metadata dialog straight from the file
The details dialog only showed the database copy of MediaMetadata.raw(). Add MetadataReader.readAll, returning (group, name, value) entries for every tag of every directory, each XMP property and directory errors, and expose it as MediaAnalysisService.readAllMetadata. GalleryView runs that read on the METADATA pool as a silent tracked task; the dialog opens at once with a spinner over the disabled table, then shows the entries in a group/name/value table or the read error. Closing it cancels the read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package org.icroco.pholio.domain.media;
|
||||
|
||||
/**
|
||||
* One metadata value exactly as read from a file, for display — see {@link MetadataReader#readAll}.
|
||||
*
|
||||
* @param group the block the value came from, as the file names it ({@code "Exif IFD0"}, {@code "GPS"},
|
||||
* {@code "XMP"}…)
|
||||
* @param name the tag or property name within that block
|
||||
* @param value its human-readable rendering ({@code "1/125 sec"} rather than {@code 0.008})
|
||||
*/
|
||||
public record MetadataEntry(String group, String name, String value) {
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.icroco.pholio.domain.media;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Reads the metadata of one file.
|
||||
@@ -25,4 +26,25 @@ public interface MetadataReader {
|
||||
* @throws MediaAccessException when the file is missing, unreadable, or malformed for {@code format}
|
||||
*/
|
||||
MediaMetadata read(Path file, ImageFormat format);
|
||||
|
||||
/**
|
||||
* Every value {@code file} carries, in file order, for a "show me everything" view — not only what
|
||||
* {@link #read} models, and read straight from the file rather than from whatever the library stored.
|
||||
*
|
||||
* <p>The default derives entries from {@link MediaMetadata#raw()}, whose {@code "Group/Name"} keys it
|
||||
* splits back apart; an implementation able to list more (repeated blocks, XMP properties) overrides it.
|
||||
*
|
||||
* @throws MediaAccessException same as {@link #read}
|
||||
*/
|
||||
default List<MetadataEntry> readAll(Path file, ImageFormat format) {
|
||||
return read(file, format).raw().entrySet().stream()
|
||||
.map(entry -> {
|
||||
String key = entry.getKey();
|
||||
int separator = key.indexOf('/');
|
||||
return separator < 0
|
||||
? new MetadataEntry("", key, entry.getValue())
|
||||
: new MetadataEntry(key.substring(0, separator), key.substring(separator + 1), entry.getValue());
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.icroco.pholio.domain.media.ImageFormat;
|
||||
import org.icroco.pholio.domain.media.MediaAccessException;
|
||||
import org.icroco.pholio.domain.media.MediaMetaFactory;
|
||||
import org.icroco.pholio.domain.media.MediaMetadata;
|
||||
import org.icroco.pholio.domain.media.MetadataEntry;
|
||||
import org.icroco.pholio.infra.media.*;
|
||||
import org.icroco.pholio.infra.persistence.folder.MediaFileRepository;
|
||||
import org.icroco.pholio.infra.preferences.AppPreferences;
|
||||
@@ -21,6 +22,7 @@ import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -113,6 +115,25 @@ public class MediaAnalysisService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Every metadata value {@code absolute} carries, read from the file itself — never from the library's
|
||||
* own stored copy, which only keeps what {@link MediaMetadata} models — for the "all metadata" dialog.
|
||||
* A full header parse: call it off the FX thread.
|
||||
*
|
||||
* <p>Unlike {@link #extractMetadata}, failures propagate rather than being logged away: this backs one
|
||||
* explicit user request, which should say why it came back empty instead of looking like a file with
|
||||
* no metadata at all.
|
||||
*
|
||||
* @throws MediaAccessException when the format is no longer recognised or the file cannot be read
|
||||
*/
|
||||
public List<MetadataEntry> readAllMetadata(Path absolute) {
|
||||
ImageFormat format = formats.formatOf(absolute)
|
||||
.orElseThrow(() -> new MediaAccessException(absolute, "Not a recognised format"));
|
||||
MediaMetaFactory factory = formats.factoryFor(format)
|
||||
.orElseThrow(() -> new MediaAccessException(absolute, "No reader for " + format));
|
||||
return factory.reader().readAll(absolute, format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a fresh decode/resize/encode of {@code absolute}'s thumbnail even though one may already sit
|
||||
* cached for {@code file}'s hash — {@link #generateThumbnail} on its own would just reuse whatever is
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.nio.file.Path;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
@@ -115,6 +116,39 @@ final class MetadataExtractorReader implements MetadataReader {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Every tag of every directory, in the order metadata-extractor walked the file — unlike
|
||||
* {@link #rawTagsOf}, which keys on {@code "Directory/Tag"} and so keeps only the first of two
|
||||
* same-named directories (a second IFD, a second XMP packet). The XMP directory's own tags are only
|
||||
* bookkeeping ({@code "XMP Value Count"}), so its actual properties are listed too, one entry each
|
||||
* ({@code dc:subject[1]}, {@code GCamera:MotionPhoto}…). Directory errors (a truncated segment, an
|
||||
* unknown maker note) are listed as well: they explain why a value someone expected is missing.
|
||||
*/
|
||||
@Override
|
||||
public List<MetadataEntry> readAll(Path file, ImageFormat format) {
|
||||
List<MetadataEntry> entries = new ArrayList<>();
|
||||
for (Directory directory : extract(file).getDirectories()) {
|
||||
String group = directory.getName();
|
||||
for (Tag tag : directory.getTags()) {
|
||||
String description = tag.getDescription();
|
||||
if (description != null && !description.isBlank()) {
|
||||
entries.add(new MetadataEntry(group, tag.getTagName(), description.strip()));
|
||||
}
|
||||
}
|
||||
if (directory instanceof XmpDirectory xmp) {
|
||||
new TreeMap<>(xmp.getXmpProperties()).forEach((path, value) -> {
|
||||
if (value != null && !value.isBlank()) {
|
||||
entries.add(new MetadataEntry(group, path, value.strip()));
|
||||
}
|
||||
});
|
||||
}
|
||||
for (String error : directory.getErrors()) {
|
||||
entries.add(new MetadataEntry(group, "Error", error));
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static Metadata extract(Path file) {
|
||||
if (!Files.isRegularFile(file)) {
|
||||
throw new MediaAccessException(file, "Not a readable file");
|
||||
|
||||
@@ -17,6 +17,7 @@ import javafx.util.Subscription;
|
||||
import org.icroco.pholio.domain.library.MediaFile;
|
||||
import org.icroco.pholio.domain.media.GeoLocation;
|
||||
import org.icroco.pholio.domain.media.MediaMetadata;
|
||||
import org.icroco.pholio.domain.media.MetadataEntry;
|
||||
import org.icroco.pholio.infra.geocoding.IPlaceSearchService;
|
||||
import org.icroco.pholio.infra.geocoding.RecentLocationService;
|
||||
import org.icroco.pholio.infra.library.LibraryFolderService;
|
||||
@@ -25,6 +26,7 @@ import org.icroco.pholio.infra.library.MediaFileService;
|
||||
import org.icroco.pholio.infra.library.MediaMetadataEditService;
|
||||
import org.icroco.pholio.infra.preferences.AppPreferences;
|
||||
import org.icroco.pholio.infra.recognition.FaceRegionQueryService;
|
||||
import org.icroco.pholio.infra.task.TaskBody;
|
||||
import org.icroco.pholio.infra.task.TaskService;
|
||||
import org.icroco.pholio.infra.task.TaskType;
|
||||
import org.icroco.pholio.ui.common.Disposable;
|
||||
@@ -56,6 +58,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
/**
|
||||
@@ -489,13 +492,38 @@ public class GalleryView extends HBox implements Disposable, SelectionSource, Na
|
||||
|
||||
/**
|
||||
* {@link MediaInfoPane#setOnShowDetails}'s target — read-only, so unlike every other dialog opened from
|
||||
* here there is nothing to write back; {@link MediaMetadataDetailsView#show} itself hides the modal on
|
||||
* both its close button and Escape.
|
||||
* here there is nothing to write back. The dialog opens at once in its loading state while
|
||||
* {@link MediaAnalysisService#readAllMetadata} parses the file itself on {@link TaskType#METADATA}
|
||||
* (tracked but silent: the dialog already shows it running); closing it before the read completes
|
||||
* cancels the read, and a late result is then simply dropped.
|
||||
*/
|
||||
private void openMetadataDetailsDialog(MediaFile file) {
|
||||
MediaMetadataDetailsView view = new MediaMetadataDetailsView(i18n);
|
||||
Optional<Path> absolute = libraryFolderService.absolutePathOf(file);
|
||||
CompletableFuture<List<MetadataEntry>> read = absolute
|
||||
.map(path -> taskService.submit(TaskType.METADATA,
|
||||
i18n.get("gallery.info.details.loading"),
|
||||
(TaskBody<List<MetadataEntry>>) progress -> mediaAnalysisService.readAllMetadata(path),
|
||||
true))
|
||||
.orElseGet(() -> CompletableFuture.failedFuture(
|
||||
new IllegalStateException(i18n.get("gallery.info.details.noFile"))));
|
||||
modalService.show(view, true);
|
||||
view.show(file, modalService::hide);
|
||||
view.show(file, () -> {
|
||||
read.cancel(true);
|
||||
modalService.hide();
|
||||
});
|
||||
read.whenComplete((entries, error) -> Platform.runLater(() -> {
|
||||
if (read.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
if (error != null) {
|
||||
Throwable cause = error instanceof CompletionException && error.getCause() != null ? error.getCause() : error;
|
||||
log.warn("Could not read metadata of media file {}", file.id(), cause);
|
||||
view.showError(cause.getMessage() != null ? cause.getMessage() : cause.toString());
|
||||
} else {
|
||||
view.showEntries(entries);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,31 +5,33 @@ import atlantafx.base.theme.Styles;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.icroco.pholio.domain.library.MediaFile;
|
||||
import org.icroco.pholio.domain.media.MediaMetadata;
|
||||
import org.icroco.pholio.ui.common.Disposable;
|
||||
import org.icroco.pholio.domain.media.MetadataEntry;
|
||||
import org.icroco.pholio.infra.i18n.I18nService;
|
||||
import org.icroco.pholio.ui.common.Disposable;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* "All metadata" dialog — every entry of {@link MediaMetadata#raw()} (see that field's own javadoc: "the
|
||||
* inspector can display tags nobody anticipated") in a plain two-column table, name then value, for
|
||||
* whichever {@link MediaFile} {@link MediaInfoPane}'s own details icon was clicked for.
|
||||
* "All metadata" dialog — every value the file itself carries ({@link MetadataEntry}: group, name, value),
|
||||
* read fresh from disk by {@code MediaAnalysisService.readAllMetadata} rather than from the library's own
|
||||
* stored copy, which only keeps what {@code MediaMetadata} models.
|
||||
*
|
||||
* <p>That read is a full header parse, run off the FX thread by {@link GalleryView}, so the dialog opens
|
||||
* straight away in its loading state ({@link #showLoading}) — a spinner over the table, the table itself
|
||||
* disabled — and {@link GalleryView} then hands it either {@link #showEntries} or {@link #showError}. The
|
||||
* modal scrim around it already keeps the rest of the application out of reach meanwhile; only closing
|
||||
* stays available, which also abandons the read.
|
||||
*
|
||||
* <p>Read-only, unlike {@link CapturedAtEditView}/{@link GeoLocationEditView} which this is otherwise built
|
||||
* the same way as (a fresh instance per invocation, configured through {@link #show}): there is nothing
|
||||
@@ -37,15 +39,18 @@ import java.util.Map;
|
||||
*/
|
||||
public class MediaMetadataDetailsView extends VBox implements Disposable {
|
||||
|
||||
private static final double WIDTH = 480;
|
||||
private static final double HEIGHT = 560;
|
||||
private static final double WIDTH = 640;
|
||||
private static final double HEIGHT = 600;
|
||||
|
||||
private final I18nService i18n;
|
||||
|
||||
private final Label title = new Label();
|
||||
private final Button closeButton = new Button();
|
||||
private final TableView<Map.Entry<String, String>> table = new TableView<>();
|
||||
private final Button doneButton = new Button();
|
||||
private final Label title = new Label();
|
||||
private final Label fileName = new Label();
|
||||
private final Button closeButton = new Button();
|
||||
private final TableView<MetadataEntry> table = new TableView<>();
|
||||
private final Label placeholder = new Label();
|
||||
private final VBox loading;
|
||||
private final Label loadingLabel = new Label();
|
||||
private final Button doneButton = new Button();
|
||||
private final I18nService i18n;
|
||||
|
||||
private Runnable onClose = () -> {};
|
||||
|
||||
@@ -64,32 +69,35 @@ public class MediaMetadataDetailsView extends VBox implements Disposable {
|
||||
icon.setIconSize(24);
|
||||
title.getStyleClass().add(Styles.TITLE_4);
|
||||
title.textProperty().bind(i18n.binding("gallery.info.details.title"));
|
||||
fileName.getStyleClass().add(Styles.TEXT_MUTED);
|
||||
VBox titles = new VBox(2, title, fileName);
|
||||
|
||||
closeButton.setGraphic(FontIcon.of(Feather.X, 16));
|
||||
closeButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
closeButton.getStyleClass().add("photo-detail-icon-button");
|
||||
closeButton.setOnAction(event -> onClose.run());
|
||||
|
||||
HBox header = new HBox(10, icon, title, new Spacer(), closeButton);
|
||||
HBox header = new HBox(10, icon, titles, new Spacer(), closeButton);
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
TableColumn<Map.Entry<String, String>, String> nameColumn = new TableColumn<>();
|
||||
nameColumn.textProperty().bind(i18n.binding("gallery.info.details.name"));
|
||||
nameColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getKey()));
|
||||
nameColumn.setPrefWidth(170);
|
||||
table.getColumns().addAll(List.of(column("gallery.info.details.group", 150, MetadataEntry::group),
|
||||
column("gallery.info.details.name", 180, MetadataEntry::name),
|
||||
column("gallery.info.details.value", 260, MetadataEntry::value)));
|
||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
|
||||
placeholder.getStyleClass().add(Styles.TEXT_MUTED);
|
||||
placeholder.setWrapText(true);
|
||||
table.setPlaceholder(placeholder);
|
||||
|
||||
TableColumn<Map.Entry<String, String>, String> valueColumn = new TableColumn<>();
|
||||
valueColumn.textProperty().bind(i18n.binding("gallery.info.details.value"));
|
||||
valueColumn.setCellValueFactory(data -> new SimpleStringProperty(data.getValue().getValue()));
|
||||
valueColumn.setPrefWidth(280);
|
||||
ProgressIndicator spinner = new ProgressIndicator();
|
||||
spinner.setPrefSize(48, 48);
|
||||
loadingLabel.textProperty().bind(i18n.binding("gallery.info.details.loading"));
|
||||
loadingLabel.getStyleClass().add(Styles.TEXT_MUTED);
|
||||
loading = new VBox(12, spinner, loadingLabel);
|
||||
loading.setAlignment(Pos.CENTER);
|
||||
loading.getStyleClass().add("metadata-details-loading");
|
||||
|
||||
table.getColumns().addAll(nameColumn, valueColumn);
|
||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
VBox.setVgrow(table, Priority.ALWAYS);
|
||||
Label noRows = new Label();
|
||||
noRows.textProperty().bind(i18n.binding("gallery.info.noMetadata"));
|
||||
noRows.getStyleClass().add(Styles.TEXT_MUTED);
|
||||
table.setPlaceholder(noRows);
|
||||
StackPane body = new StackPane(table, loading);
|
||||
VBox.setVgrow(body, Priority.ALWAYS);
|
||||
|
||||
doneButton.textProperty().bind(i18n.binding("action.close"));
|
||||
doneButton.getStyleClass().add(Styles.ACCENT);
|
||||
@@ -105,20 +113,57 @@ public class MediaMetadataDetailsView extends VBox implements Disposable {
|
||||
}
|
||||
});
|
||||
|
||||
getChildren().addAll(header, table, new Separator(), footer);
|
||||
getChildren().addAll(header, body, new Separator(), footer);
|
||||
showLoading();
|
||||
}
|
||||
|
||||
private TableColumn<MetadataEntry, String> column(String titleKey, double prefWidth,
|
||||
Function<MetadataEntry, String> value) {
|
||||
TableColumn<MetadataEntry, String> column = new TableColumn<>();
|
||||
column.textProperty().bind(i18n.binding(titleKey));
|
||||
column.setCellValueFactory(data -> new SimpleStringProperty(value.apply(data.getValue())));
|
||||
column.setPrefWidth(prefWidth);
|
||||
return column;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog for {@code file}, in its loading state until {@link #showEntries} or {@link #showError}.
|
||||
*
|
||||
* @param onClose the close button, Done and Escape alike — also expected to abandon the read if still running
|
||||
*/
|
||||
public void show(MediaFile file, Runnable onClose) {
|
||||
this.onClose = onClose;
|
||||
MediaMetadata metadata = file.metadata();
|
||||
List<Map.Entry<String, String>> rows = metadata == null ? List.of() : List.copyOf(metadata.raw().entrySet());
|
||||
table.getItems().setAll(rows);
|
||||
fileName.setText(file.path().getFileName().toString());
|
||||
showLoading();
|
||||
doneButton.requestFocus();
|
||||
}
|
||||
|
||||
public void showLoading() {
|
||||
table.getItems().clear();
|
||||
table.setDisable(true);
|
||||
loading.setVisible(true);
|
||||
}
|
||||
|
||||
public void showEntries(List<MetadataEntry> entries) {
|
||||
placeholder.setText(i18n.get("gallery.info.noMetadata"));
|
||||
table.getItems().setAll(entries);
|
||||
table.setDisable(false);
|
||||
loading.setVisible(false);
|
||||
}
|
||||
|
||||
public void showError(String message) {
|
||||
placeholder.setText(i18n.get("gallery.info.details.error", message));
|
||||
table.getItems().clear();
|
||||
table.setDisable(false);
|
||||
loading.setVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
title.textProperty().unbind();
|
||||
loadingLabel.textProperty().unbind();
|
||||
doneButton.textProperty().unbind();
|
||||
table.getColumns().forEach(column -> column.textProperty().unbind());
|
||||
closeButton.setOnAction(null);
|
||||
doneButton.setOnAction(null);
|
||||
}
|
||||
|
||||
@@ -460,6 +460,11 @@
|
||||
|
||||
/* One detected person/animal on MediaInfoPane's persons row — a pill, same rounding convention as
|
||||
.media-info-edit-button, so an unnamed cluster ("Unknown person") reads as a real value, not a link. */
|
||||
/* MediaMetadataDetailsView's loading state — covers the (disabled) table while the file is being read. */
|
||||
.metadata-details-loading {
|
||||
-fx-background-color: -color-bg-default;
|
||||
}
|
||||
|
||||
.media-info-person-chip {
|
||||
-fx-background-color: -color-bg-inset;
|
||||
-fx-background-radius: 999px;
|
||||
|
||||
@@ -114,6 +114,10 @@ gallery.info.unknownPerson=Unknown person
|
||||
gallery.info.details.title=All metadata
|
||||
gallery.info.details.name=Name
|
||||
gallery.info.details.value=Value
|
||||
gallery.info.details.group=Group
|
||||
gallery.info.details.loading=Reading metadata from the file…
|
||||
gallery.info.details.error=Could not read metadata from the file: {0}
|
||||
gallery.info.details.noFile=This file's library folder is no longer configured.
|
||||
gallery.location.edit.title=Add a location
|
||||
gallery.location.edit.searchPrompt=Search for a place…
|
||||
gallery.location.edit.hint=Changes to the place a photo was taken are saved to the library and, when the format supports it, to the file itself.
|
||||
|
||||
@@ -117,6 +117,10 @@ gallery.info.unknownPerson=Personne inconnue
|
||||
gallery.info.details.title=Toutes les métadonnées
|
||||
gallery.info.details.name=Nom
|
||||
gallery.info.details.value=Valeur
|
||||
gallery.info.details.group=Groupe
|
||||
gallery.info.details.loading=Lecture des métadonnées du fichier…
|
||||
gallery.info.details.error=Impossible de lire les métadonnées du fichier : {0}
|
||||
gallery.info.details.noFile=Le dossier de la photothèque de ce fichier n'est plus configuré.
|
||||
gallery.location.edit.title=Ajouter un lieu
|
||||
gallery.location.edit.searchPrompt=Rechercher un lieu…
|
||||
gallery.location.edit.hint=Les modifications apportées au lieu de prise de vue seront enregistrées dans la photothèque et, si le format le permet, dans le fichier lui-même.
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.assertj.core.api.SoftAssertions;
|
||||
import org.icroco.pholio.domain.media.GeoLocation;
|
||||
import org.icroco.pholio.domain.media.ImageFormat;
|
||||
import org.icroco.pholio.domain.media.MediaMetadata;
|
||||
import org.icroco.pholio.domain.media.MetadataEntry;
|
||||
import org.icroco.pholio.domain.media.MetadataReader;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
@@ -161,6 +162,19 @@ class MetadataExtractorReaderTest {
|
||||
assertThat(reader.read(file, ImageFormat.JPEG).tags()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAllListsEveryTagWithItsGroupPlusEachXmpProperty() throws IOException {
|
||||
Path file = jpegWithIptcAndXmp(List.of("beach"), List.of("sunset", "vacation"));
|
||||
|
||||
List<MetadataEntry> entries = reader.readAll(file, ImageFormat.JPEG);
|
||||
|
||||
SoftAssertions.assertSoftly(softly -> {
|
||||
softly.assertThat(entries).contains(new MetadataEntry("IPTC", "Keywords", "beach"));
|
||||
softly.assertThat(entries).contains(new MetadataEntry("XMP", "dc:subject[1]", "sunset"),
|
||||
new MetadataEntry("XMP", "dc:subject[2]", "vacation"));
|
||||
});
|
||||
}
|
||||
|
||||
private Path jpegWithIptcKeywords(String... keywords) throws IOException {
|
||||
return jpegWithIptcAndXmp(List.of(keywords), List.of());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user