diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0cc4fbe4094..4d66125d527 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -48,6 +48,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- By double clicking on a local citation in the Citation Relations Tab you can now jump the linked entry. [#11955](https://github.com/JabRef/jabref/pull/11955)
- We use the menu icon for background tasks as a progress indicator to visualise an import's progress when dragging and dropping several PDF files into the main table. [#12072](https://github.com/JabRef/jabref/pull/12072)
- The PDF content importer now supports importing title from upto the second page of the PDF. [#12139](https://github.com/JabRef/jabref/issues/12139)
+- We added the ability for users to display a cover image in the preview panel of the entry editor for book, in-book and booklet citations. [#10120](https://github.com/JabRef/jabref/issues/10120)
### Changed
diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java
index 306c543af77..00e1616d6f1 100644
--- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java
+++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java
@@ -218,14 +218,35 @@ private void setPreviewText(String text) {
layoutText = """
- %s
+
+
+ %s
+
+
+
+
+
- """.formatted(text);
+ """.formatted(text, getBookCoverURI());
highlightLayoutText();
this.setHvalue(0);
}
+ private String getBookCoverURI() {
+ if (entry != null) {
+ if (entry.getCoverImageFile().isPresent()) {
+ return "file:///" + entry.getCoverImageFile().get().getLink();
+ }
+ }
+
+ return "";
+ }
+
private void highlightLayoutText() {
if (layoutText == null) {
return;
diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java
index 6f2a38aca7c..8b1adc9c9ae 100644
--- a/src/main/java/org/jabref/model/entry/BibEntry.java
+++ b/src/main/java/org/jabref/model/entry/BibEntry.java
@@ -99,6 +99,16 @@
public class BibEntry implements Cloneable {
public static final EntryType DEFAULT_TYPE = StandardEntryType.Misc;
+
+ private static final HashSet COVERABLE_TYPES = new HashSet<>();
+ static {
+ COVERABLE_TYPES.add(StandardEntryType.Book);
+ COVERABLE_TYPES.add(StandardEntryType.InBook);
+ COVERABLE_TYPES.add(StandardEntryType.Booklet);
+ }
+
+ private static final String COVER_TAG = "cover";
+
private static final Logger LOGGER = LoggerFactory.getLogger(BibEntry.class);
private final SharedBibEntryData sharedBibEntryData;
@@ -1124,6 +1134,36 @@ public Optional addFiles(List filesToAdd) {
currentFiles.addAll(filesToAdd);
return setFiles(currentFiles);
}
+
+ /**
+ * @return LinkedFile
that contains the cover image
+ * if the BibEntry
is a Book or other COVERABLE_TYPES
+ */
+ public Optional getCoverImageFile() {
+ if (!isCoverable()) {
+ return Optional.empty();
+ }
+
+ List files = getFiles();
+
+ if (files == null) {
+ return Optional.empty();
+ }
+
+ if (files.isEmpty()) {
+ return Optional.empty();
+ }
+
+ for (LinkedFile file : getFiles()) {
+ if (file.getDescription().equalsIgnoreCase(COVER_TAG)) {
+ if (file.isImage()) {
+ return Optional.of(file);
+ }
+ }
+ }
+
+ return Optional.empty();
+ }
// endregion
/**
@@ -1253,4 +1293,11 @@ public boolean isEmpty() {
}
return StandardField.AUTOMATIC_FIELDS.containsAll(this.getFields());
}
+
+ /**
+ * @return true
if this entry's type
is a Book or one of COVERABLE_TYPES
+ */
+ private boolean isCoverable() {
+ return COVERABLE_TYPES.contains(getType());
+ }
}
diff --git a/src/main/java/org/jabref/model/entry/LinkedFile.java b/src/main/java/org/jabref/model/entry/LinkedFile.java
index ec189fab2bf..cd7167cc6d2 100644
--- a/src/main/java/org/jabref/model/entry/LinkedFile.java
+++ b/src/main/java/org/jabref/model/entry/LinkedFile.java
@@ -220,6 +220,14 @@ public boolean isOnlineLink() {
return isOnlineLink(link.get());
}
+ /**
+ * @return true
if fileType
contains the string "image"
+ */
+ public boolean isImage() {
+ // Cannot compare fileType to StandardExternalFileType enum since it is a StringProperty
+ return getFileType().toLowerCase().contains("image");
+ }
+
public Optional findIn(BibDatabaseContext databaseContext, FilePreferences filePreferences) {
List dirs = databaseContext.getFileDirectories(filePreferences);
return findIn(dirs);
diff --git a/src/test/java/org/jabref/model/entry/BibEntryTest.java b/src/test/java/org/jabref/model/entry/BibEntryTest.java
index a9910037379..3dbd7f28741 100644
--- a/src/test/java/org/jabref/model/entry/BibEntryTest.java
+++ b/src/test/java/org/jabref/model/entry/BibEntryTest.java
@@ -1,6 +1,7 @@
package org.jabref.model.entry;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -813,6 +814,82 @@ void mergeEntriesWithOverlapAndPriorityGivenToOverlappingField() {
assertEquals(expected.getFields(), copyEntry.getFields());
}
+ @Test
+ void getCoverImageReturnsCorrectImage() {
+ LinkedFile cover1 = new LinkedFile("", Paths.get("JabRef-icon-128.png"), "PNG image");
+ LinkedFile cover2 = new LinkedFile("", Paths.get("JabRef-icon-64.png"), "PNG image");
+ LinkedFile cover3 = new LinkedFile("cover", Paths.get("wallpaper.jpg"), "JPG image");
+ BibEntry entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value");
+ entry.addFile(cover1);
+ entry.addFile(cover2);
+ entry.addFile(cover3);
+ assertEquals(Optional.of(cover3), entry.getCoverImageFile());
+ }
+
+ @Test
+ void getCoverImageReturnsEmptyIfNoFiles() {
+ entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value");
+ assertEquals(Optional.empty(), entry.getCoverImageFile());
+ }
+
+ @Test
+ void getCoverImageReturnsEmptyIfNoImageFiles() {
+ LinkedFile pdf = new LinkedFile("", Paths.get("Baldoni2002.pdf").toAbsolutePath().toString(), "pdf");
+ LinkedFile markdown = new LinkedFile("", "readme.md", "md");
+ entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value");
+ entry.addFile(markdown);
+ entry.addFile(pdf);
+ assertEquals(Optional.empty(), entry.getCoverImageFile());
+ }
+
+ @ParameterizedTest
+ @MethodSource("nonCoverableEntryTypes")
+ void getCoverImageReturnsEmptyIfEntryIsNotCoverable(StandardEntryType entryType) {
+ BibEntry entry = new BibEntry(entryType).withField(StandardField.AUTHOR, "value");
+ assertEquals(Optional.empty(), entry.getCoverImageFile());
+ }
+
+ static Stream nonCoverableEntryTypes() {
+ return Stream.of(
+ StandardEntryType.Proceedings,
+ StandardEntryType.Dataset,
+ StandardEntryType.Software
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("imagesWithoutCoverDescription")
+ void getCoverImageDoesNotReturnImagesWithoutCoverDescription(LinkedFile image) {
+ entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value");
+ entry.addFile(image);
+ assertEquals(Optional.empty(), entry.getCoverImageFile());
+ }
+
+ static Stream imagesWithoutCoverDescription() {
+ return Stream.of(
+ new LinkedFile("", Paths.get("JabRef-icon-128.png"), "PNG image"),
+ new LinkedFile("", Paths.get("JabRef-icon-64.png"), "PNG image"),
+ new LinkedFile("", Paths.get("JabRef-icon-32.png"), "PNG image")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("docsWithCoverDescription")
+ void getCoverImageDoesNotReturnDocumentsWithCoverDescription(LinkedFile file) {
+ entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value");
+ entry.addFile(file);
+ assertEquals(Optional.empty(), entry.getCoverImageFile());
+ }
+
+ static Stream docsWithCoverDescription() {
+ return Stream.of(
+ new LinkedFile("cover", Paths.get("Baldoni2002.pdf"), "pdf"),
+ new LinkedFile("cover", Paths.get("readme.md"), "md"),
+ new LinkedFile("cover", Paths.get("BiblioscapeImporterTestArticleST.txt"), "txt"),
+ new LinkedFile("cover", Paths.get("emptyFile.xml"), "xml")
+ );
+ }
+
public static Stream isEmpty() {
return Stream.of(
new BibEntry(),