Skip to content
Draft
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
d29ab3b
Added initial work to get basic image covers working properly
bblhd Oct 16, 2025
7b33850
Merge branch 'JabRef:main' into feature/book-front-cover-10120
bblhd Oct 16, 2025
05d21d4
Merge branch 'main' of https://github.com/JabRef/jabref into feature/…
bblhd Oct 18, 2025
2865553
fixed order of cover images, refactored some of the cover code
bblhd Oct 18, 2025
7842c6e
rudimentary cover fetching
bblhd Nov 12, 2025
3f91954
changed tabs to spaces
bblhd Nov 12, 2025
bb58664
merged with upstream
bblhd Nov 12, 2025
112a095
fixed problems and changed cover tag parenthesis for display
bblhd Nov 12, 2025
56475f3
fixed small issues, re-re-factoring
bblhd Nov 15, 2025
68e55c6
incorrect name, now getCoverImageFileFromStringOfISBN
bblhd Nov 15, 2025
a9e8120
merged with upstream
bblhd Nov 15, 2025
71fba83
fixed pattern naming somewhat, improved handling of edge cases and ad…
bblhd Nov 16, 2025
0d97ac5
Added preferences for cover images
bblhd Nov 16, 2025
25b996c
merge from upstream
bblhd Nov 16, 2025
f4946cd
Merge branch 'main' into feature/book-front-cover-10120
Siedlerchr Nov 17, 2025
2c61ef3
Fix submodule changes
bblhd Nov 17, 2025
dc614c4
style changes
bblhd Nov 17, 2025
5e9b8af
fixes to getSuggestedFileName and added localization for new text
bblhd Nov 17, 2025
2d9abbc
Merge branch 'main' into feature/book-front-cover-10120
bblhd Nov 17, 2025
b56b664
fixed problem in getSuggestedFileName
bblhd Nov 17, 2025
45ff913
further fixes
bblhd Nov 17, 2025
4f6a3b6
fixed missing period in suggested filename
bblhd Nov 17, 2025
8b05127
fixed bad LinkedList initializer in test and missed whitespace
bblhd Nov 17, 2025
9d705fc
brought behaviour of getSuggestedFileName(extension) in line with get…
bblhd Nov 17, 2025
73831aa
fixed errors in test
bblhd Nov 17, 2025
d7a6537
removed unused import
bblhd Nov 17, 2025
692db5f
fixed test cases and some rewrite checks
bblhd Nov 17, 2025
23f3203
fixed test name collision
bblhd Nov 17, 2025
c5d7d7e
reversed my misunderstanding of org.openrewrite.java.migrate.util.Opt…
bblhd Nov 17, 2025
e5df521
added pull request to changelog, added tests for getFileNameFromUrl, …
bblhd Nov 17, 2025
b3473b0
Merge branch 'main' into feature/book-front-cover-10120
bblhd Nov 17, 2025
77398d8
removed wrong arguments from test
bblhd Nov 17, 2025
ca5dcb2
removed wrong arguments from test
bblhd Nov 17, 2025
cd33ce2
improved behaviour of BookCoverFetcher when no directory available
bblhd Nov 18, 2025
df05e83
fix for changed argument order
bblhd Nov 18, 2025
a41f391
filled catch block
bblhd Nov 18, 2025
9ef2bb4
changed uses of path.tofile().method() to Files.method(path) where po…
bblhd Nov 20, 2025
cbb3340
added mime type detection for existing book covers
bblhd Nov 20, 2025
4124d98
fixed import order
bblhd Nov 20, 2025
4f6607f
Merge branch 'main' into feature/book-front-cover-10120
bblhd Nov 20, 2025
99fe001
missing semicolon
bblhd Nov 20, 2025
777ad41
fixed new variable and removed superfluous check
bblhd Nov 20, 2025
9fc23be
missing argument somehow
bblhd Nov 20, 2025
966fd67
Update jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.…
bblhd Nov 20, 2025
c68ef91
initial rework of covers, away from using attached files
bblhd Nov 20, 2025
bc9f0b4
Merge branch 'main' into feature/book-front-cover-10120
bblhd Nov 20, 2025
622838b
replaced findFileByNameWithAnyExtension with simpler code that is eas…
bblhd Nov 20, 2025
ca67eb3
removed LinkedFile#getURI/getURL due to lack of use
bblhd Nov 20, 2025
3ebdc55
improved test sources
bblhd Nov 20, 2025
f2d7a1a
fixed test formating
bblhd Nov 20, 2025
7f17cd9
fixed name of getValidFileName
bblhd Nov 20, 2025
477d845
fixed missing .get() for Optional
bblhd Nov 20, 2025
6824940
fixed missing import
bblhd Nov 20, 2025
b0be871
fixed things missed in book cover changes
bblhd Nov 20, 2025
609371a
better use of map
bblhd Nov 20, 2025
5813bcc
fixed syntax
bblhd Nov 20, 2025
11b3570
corrected bad definition and removed createFileNameFromPattern "defau…
bblhd Nov 20, 2025
fb1deab
Merge branch 'main' into feature/book-front-cover-10120
bblhd Nov 20, 2025
2cb5c99
minor BookCoverFetcher fixes
bblhd Nov 20, 2025
05d3561
more usage of map, implemented basic seperation of cover images from …
bblhd Nov 26, 2025
ef72987
Merge branch 'main' into feature/book-front-cover-10120
bblhd Nov 26, 2025
8b87815
fixed mangled merge
bblhd Nov 26, 2025
d642f6c
fixed test using Argument.of rather than Arguments.of
bblhd Nov 26, 2025
0d474a3
more Arguments fix, removed unused imports, other syntax fixes
bblhd Nov 26, 2025
8f39853
fixes for wrong import and made getFileNameFromUrl return Optional, p…
bblhd Nov 26, 2025
3d9b17f
fixed missing character when parsing urls, removed bad test case
bblhd Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added the integrity check to the jabkit cli application. [#13848](https://github.com/JabRef/jabref/issues/13848)
- We added support for Cygwin-file paths on a Windows Operating System. [#13274](https://github.com/JabRef/jabref/issues/13274)
- We fixed an issue where "Print preview" would throw a `NullPointerException` if no printers were available. [#13708](https://github.com/JabRef/jabref/issues/13708)
- We added "IEEE" as another option for parsing plain text citations. [#14233](github.com/JabRef/jabref/pull/14233)
- We added cover images for books, which will display in entry previews if available, and can be automatically downloaded when adding an entry via ISBN. [#10120](https://github.com/JabRef/jabref/issues/10120)
- We added "IEEE" as another option for parsing plain text citations. [#14233](https://github.com/JabRef/jabref/pull/14233)
- We added automatic date-based groups that create year/month/day subgroups from an entry’s date fields. [#10822](https://github.com/JabRef/jabref/issues/10822)
- We added `doi-to-bibtex` to `JabKit`. [#14244](https://github.com/JabRef/jabref/pull/14244)
- We added `--provider=crossref` to `get-cited-works` at `JabKit`. [#14357](https://github.com/JabRef/jabref/pull/14357)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ public List<BibEntry> handleStringData(String data) throws FetcherException {
LOGGER.trace("Checking if URL is a PDF: {}", data);

if (URLUtil.isURL(data)) {
String fileName = data.substring(data.lastIndexOf('/') + 1);
String fileName = FileUtil.getFileNameFromUrl(data);
if (FileUtil.isPDFFile(Path.of(fileName))) {
try {
return handlePdfUrl(data);
Expand Down Expand Up @@ -481,7 +481,10 @@ private List<BibEntry> handlePdfUrl(String pdfUrl) throws IOException {
return List.of();
}
URLDownload urlDownload = new URLDownload(pdfUrl);
String filename = URLUtil.getFileNameFromUrl(pdfUrl);
String filename = FileUtil.getFileNameFromUrl(pdfUrl);
if (filename.isBlank()) {
filename = "downloaded.pdf";
}
Path targetFile = targetDirectory.get().resolve(filename);
try {
urlDownload.toFile(targetFile);
Expand Down
127 changes: 127 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.jabref.gui.importer;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.InvalidPathException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jabref.gui.externalfiletype.ExternalFileType;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.frame.ExternalApplicationsPreferences;
import org.jabref.logic.FilePreferences;
import org.jabref.logic.importer.FetcherException;
import org.jabref.logic.net.URLDownload;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.entry.identifier.ISBN;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Provides functions for downloading and retrieving book covers for entries.
*/
public class BookCoverFetcher {

private static final Logger LOGGER = LoggerFactory.getLogger(BookCoverFetcher.class);

private static final Pattern URL_JSON_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$");

private static final String URL_FETCHER_URL = "https://bookcover.longitood.com/bookcover/";
private static final String IMAGE_FALLBACK_URL = "https://covers.openlibrary.org/b/isbn/";
private static final String IMAGE_FALLBACK_SUFFIX = "-L.jpg";

private final ExternalApplicationsPreferences externalApplicationsPreferences;

public BookCoverFetcher(ExternalApplicationsPreferences externalApplicationsPreferences) {
this.externalApplicationsPreferences = externalApplicationsPreferences;
}

public Optional<Path> getDownloadedCoverForEntry(BibEntry entry, Path directory) {
Optional<ISBN> isbn = entry.getISBN();
if (isbn.isPresent()) {
final String name = "isbn-" + isbn.get().asString();
return findExistingImage(name, directory);
}
return Optional.empty();
}

public void downloadCoversForEntry(BibEntry entry, Path directory) {
Optional<ISBN> isbn = entry.getISBN();
if (isbn.isPresent()) {
final String name = "isbn-" + isbn.get().asString();
if (findExistingImage(name, directory).isEmpty()) {
final String url = getSourceForIsbn(isbn.get());
Optional<LinkedFile> file = downloadCoverImage(url, name, directory);
}
}
}

private Optional<Path> findExistingImage(String name, Path directory) {
for (ExternalFileType filetype : externalApplicationsPreferences.getExternalFileTypes()) {
if (filetype.getMimeType().startsWith("image/")) {
Path path = directory.resolve(FileUtil.getValidFileName(name + "." + t.getExtension()));
if (Files.exists(path)) {
return Optional.of(path);
}
}
}
return Optional.empty();
}

private void downloadCoverImage(String url, String name, Path directory) {
Optional<String> extension = FileUtil.getFileExtension(FileUtil.getFileNameFromUrl(url));

try {
LOGGER.info("Downloading cover image file from {}", url);

URLDownload download = new URLDownload(url);
Optional<String> mime = download.getMimeType();

Optional<ExternalFileType> suggested = Optional.empty();
if (mime.isPresent()) {
suggested = ExternalFileTypes.getExternalFileTypeByMimeType(mime.get(), externalApplicationsPreferences);
}
if (suggested.isEmpty() && extension.isPresent()) {
suggested = ExternalFileTypes.getExternalFileTypeByExt(extension.get(), externalApplicationsPreferences);
}

if (suggested.isPresent()) {
if (suggested.get().getMimeType().startsWith("image/")) {
download.toFile(directory.resolve(FileUtil.getValidFileName(name + "." + suggested.get().getExtension())));
}
}
} catch (FetcherException | MalformedURLException e) {
LOGGER.error("Error while downloading cover image file", e);
}
}

private static String getSourceForIsbn(ISBN isbn) {
if (isbn.isIsbn13()) {
String url = URL_FETCHER_URL + isbn.asString();
try {
LOGGER.info("Downloading book cover url from {}", url);

URLDownload download = new URLDownload(url);
String json = download.asString();
Matcher matches = URL_JSON_PATTERN.matcher(json);

if (matches.find()) {
String coverUrlString = matches.group(1);
if (coverUrlString != null) {
return coverUrlString;
}
}
} catch (FetcherException | MalformedURLException e) {
LOGGER.error("Error while querying cover url, using fallback", e);
}
}
return IMAGE_FALLBACK_URL + isbn.asString() + IMAGE_FALLBACK_SUFFIX;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ private BackgroundTask<Path> prepareDownloadTask(Path targetDirectory, URLDownlo
if (this.suggestedName.isEmpty()) {
Optional<ExternalFileType> suggestedType = inferFileType(urlDownload);
ExternalFileType externalFileType = suggestedType.orElse(StandardExternalFileType.PDF);
suggestedName = linkedFileHandler.getSuggestedFileName(externalFileType.getExtension());
suggestedName = linkedFileHandler.getSuggestedFileName(Optional.of(externalFileType.getExtension()));
} else {
suggestedName = this.suggestedName;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.jabref.gui.LibraryTab;
import org.jabref.gui.StateManager;
import org.jabref.gui.externalfiles.ImportHandler;
import org.jabref.gui.importer.BookCoverFetcher;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.ai.AiService;
Expand Down Expand Up @@ -64,6 +65,8 @@ public class NewEntryViewModel {
private final AiService aiService;
private final FileUpdateMonitor fileUpdateMonitor;

private final BookCoverFetcher bookCoverFetcher;

private final BooleanProperty executing;
private final BooleanProperty executedSuccessfully;

Expand Down Expand Up @@ -102,6 +105,8 @@ public NewEntryViewModel(GuiPreferences preferences,
this.aiService = aiService;
this.fileUpdateMonitor = fileUpdateMonitor;

this.bookCoverFetcher = new BookCoverFetcher(preferences.getExternalApplicationsPreferences());

executing = new SimpleBooleanProperty(false);
executedSuccessfully = new SimpleBooleanProperty(false);
doiCache = new HashMap<>();
Expand Down Expand Up @@ -232,6 +237,14 @@ public ReadOnlyBooleanProperty bibtexTextValidatorProperty() {
return bibtexTextValidator.getValidationStatus().validProperty();
}

private BibEntry withCoversDownloaded(BibEntry entry) {
if (preferences.getFilePreferences().shouldDownloadCovers()) {
Path directory = Path.of(preferences.getFilePreferences().coversDownloadLocation());
bookCoverFetcher.downloadCoversForEntry(entry, directory, preferences.getExternalApplicationsPreferences());
}
return entry;
}

private class WorkerLookupId extends Task<Optional<BibEntry>> {
@Override
protected Optional<BibEntry> call() throws FetcherException {
Expand All @@ -242,7 +255,7 @@ protected Optional<BibEntry> call() throws FetcherException {
return Optional.empty();
}

return fetcher.performSearchById(text);
return fetcher.performSearchById(text).map(this::withCoversDownloaded);
}
}

Expand All @@ -257,7 +270,7 @@ protected Optional<BibEntry> call() throws FetcherException {
return Optional.empty();
}

return fetcher.performSearchById(text);
return fetcher.performSearchById(text).map(this::withCoversDownloaded);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;

Expand All @@ -25,6 +26,11 @@ public class LinkedFilesTab extends AbstractPreferenceTabView<LinkedFilesTabView

@FXML private TextField mainFileDirectory;
@FXML private RadioButton useMainFileDirectory;

@FXML private CheckBox bookCoverDownload;
@FXML private Label bookCoverLabel;
@FXML private TextField bookCoverLocation;

@FXML private RadioButton useBibLocationAsPrimary;
@FXML private Button browseDirectory;
@FXML private Button autolinkRegexHelp;
Expand Down Expand Up @@ -63,7 +69,11 @@ public void initialize() {
mainFileDirectory.disableProperty().bind(viewModel.useBibLocationAsPrimaryProperty());
browseDirectory.disableProperty().bind(viewModel.useBibLocationAsPrimaryProperty());
useBibLocationAsPrimary.selectedProperty().bindBidirectional(viewModel.useBibLocationAsPrimaryProperty());
useMainFileDirectory.selectedProperty().bindBidirectional(viewModel.useMainFileDirectoryProperty());

bookCoverLocation.textProperty().bindBidirectional(viewModel.coversDownloadLocationProperty());
bookCoverLocation.disableProperty().bind(viewModel.shouldDownloadCoversProperty().not());
bookCoverLabel.disableProperty().bind(viewModel.shouldDownloadCoversProperty().not());
bookCoverDownload.selectedProperty().bindBidirectional(viewModel.shouldDownloadCoversProperty());

moveToTrash.selectedProperty().bindBidirectional(viewModel.moveToTrashProperty());
moveToTrash.setDisable(!NativeDesktop.get().moveToTrashSupported());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public class LinkedFilesTabViewModel implements PreferenceTabViewModel {
private final BooleanProperty moveToTrashProperty = new SimpleBooleanProperty();
private final BooleanProperty openFileExplorerInFilesDirectory = new SimpleBooleanProperty();
private final BooleanProperty openFileExplorerInLastDirectory = new SimpleBooleanProperty();
private final BooleanProperty shouldDownloadCovers = new SimpleBooleanProperty();
private final StringProperty coversDownloadLocation = new SimpleStringProperty("");

private final Validator mainFileDirValidator;

Expand Down Expand Up @@ -90,6 +92,8 @@ public void setValues() {
moveToTrashProperty.setValue(filePreferences.moveToTrash());
openFileExplorerInFilesDirectory.setValue(filePreferences.shouldOpenFileExplorerInFileDirectory());
openFileExplorerInLastDirectory.setValue(filePreferences.shouldOpenFileExplorerInLastUsedDirectory());
shouldDownloadCovers.setValue(filePreferences.shouldDownloadCovers());
coversDownloadLocation.setValue(filePreferences.coversDownloadLocation());

// Autolink preferences
switch (autoLinkPreferences.getCitationKeyDependency()) {
Expand All @@ -115,6 +119,8 @@ public void storeSettings() {
filePreferences.setFulltextIndexLinkedFiles(fulltextIndex.getValue());
filePreferences.setOpenFileExplorerInFileDirectory(openFileExplorerInFilesDirectory.getValue());
filePreferences.setOpenFileExplorerInLastUsedDirectory(openFileExplorerInLastDirectory.getValue());
filePreferences.setShouldDownloadCovers(shouldDownloadCovers.getValue());
filePreferences.setOpenFileExplorerInLastUsedDirectory(coversDownloadLocation.getValue());

// Autolink preferences
if (autolinkFileStartsBibtexProperty.getValue()) {
Expand Down Expand Up @@ -216,5 +222,13 @@ public BooleanProperty openFileExplorerInFilesDirectoryProperty() {
public BooleanProperty openFileExplorerInLastDirectoryProperty() {
return openFileExplorerInLastDirectory;
}

public BooleanProperty shouldDownloadCoversProperty() {
return shouldDownloadCovers;
}

public StringProperty coversDownloadLocationProperty() {
return coversDownloadLocation;
}
}

37 changes: 31 additions & 6 deletions jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.net.MalformedURLException;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
Expand All @@ -20,6 +21,9 @@
import org.jabref.gui.StateManager;
import org.jabref.gui.desktop.os.NativeDesktop;
import org.jabref.gui.exporter.ExportToClipboardAction;
import org.jabref.gui.externalfiletype.ExternalFileType;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.gui.importer.BookCoverFetcher;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.search.Highlighter;
import org.jabref.gui.theme.ThemeManager;
Expand All @@ -30,9 +34,12 @@
import org.jabref.logic.preview.PreviewLayout;
import org.jabref.logic.util.BackgroundTask;
import org.jabref.logic.util.TaskExecutor;
import org.jabref.logic.util.io.FileUtil;
import org.jabref.logic.util.strings.StringUtil;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.entry.types.StandardEntryType;
import org.jabref.model.search.query.SearchQuery;

import com.airhacks.afterburner.injection.Injector;
Expand Down Expand Up @@ -81,6 +88,8 @@ function getSelectionHtml() {
private final StringProperty searchQueryProperty;
private final GuiPreferences preferences;

private final BookCoverFetcher bookCoverFetcher;

private @Nullable BibDatabaseContext databaseContext;
private @Nullable BibEntry entry;
private PreviewLayout layout;
Expand All @@ -105,6 +114,8 @@ public PreviewViewer(DialogService dialogService,
this.searchQueryProperty = searchQueryProperty;
this.searchQueryProperty.addListener((_, _, _) -> highlightLayoutText());

this.bookCoverFetcher = new BookCoverFetcher(preferences.getExternalApplicationsPreferences());

setFitToHeight(true);
setFitToWidth(true);
previewView = WebViewStore.get();
Expand Down Expand Up @@ -222,17 +233,31 @@ private String formatError(BibEntry entry, Throwable exception) {
}

private void setPreviewText(String text) {
String coverIfAny = "";
Optional<String> image = getCoverImageURL();
if (image.isPresent()) {
coverIfAny = "<img style=\"border-width:1px; border-style:solid; border-color:black; display:block; height:12rem;\" src=\"%s\"> <br>".formatted(image);
}

layoutText = """
<html>
<body id="previewBody">
<div id="content"> %s </div>
</body>
</html>
""".formatted(text);
<html>
<body id="previewBody">
%s <div id="content"> %s </div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is OK for a quick hack.

One should introduce a LayoutFormatter here. And then use \begin{file}\format[CoverImage}\end{file}.

The default preview style needs to be adapted accordingly.

Not sure whether we should make exceptions of the general preview handling for this because of images. @InAnYan

</body>
</html>
""".formatted(coverIfAny, text);
highlightLayoutText();
setHvalue(0);
}

private Optional<String> getCoverImageURL() {
if (entry != null) {
Path directory = Path.of(preferences.getFilePreferences().coversDownloadLocation());
return bookCoverFetcher.getDownloadedCoverForEntry(entry, location, preferences.getExternalApplicationsPreferences()).map(p -> p.toUri().toString());
}
return Optional.empty();
}

private void highlightLayoutText() {
if (layoutText == null) {
return;
Expand Down
Loading
Loading