Skip to content
Draft
Show file tree
Hide file tree
Changes from 14 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
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
131 changes: 131 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,131 @@
package org.jabref.gui.importer;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.io.File;
import java.nio.file.Path;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import org.apache.hc.core5.net.URIBuilder;

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

import kong.unirest.core.UnirestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Searches web resources for bibliographic information.
*/
public class BookCoverFetcher {

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

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

public static Optional<BibEntry> withAttachedCoverFileIfExists(Optional<BibEntry> possible, BibDatabaseContext databaseContext, FilePreferences filePreferences, ExternalApplicationsPreferences externalApplicationsPreferences) {
if (possible.isPresent() && filePreferences.shouldDownloadCovers()) {
BibEntry entry = possible.get();
Optional<ISBN> isbn = entry.getISBN();
if (isbn.isPresent()) {
final String url = getCoverImageURLForIsbn(isbn.get());
final Path directory = databaseContext.getFirstExistingFileDir(filePreferences).orElse(filePreferences.getWorkingDirectory());

// Cannot use pattern for name, as auto-generated citation keys aren't available where function is used (org.jabref.gui.newentry.NewEntryViewModel#withCoversAttached)
final String name = "isbn-"+isbn.get().asString();

Optional<LinkedFile> file = tryToDownloadLinkedFile(externalApplicationsPreferences, directory, url, filePreferences.coversDownloadLocation(), name);
if (file.isPresent()) {
entry.addFile(file.get());
}
}
possible = Optional.of(entry);
}
return possible;
}

private static String getCoverImageURLForIsbn(ISBN isbn) {
if (isbn.isIsbn13()) {
String url = "https://bookcover.longitood.com/bookcover/" + isbn.asString();
try {
LOGGER.info("Downloading book cover url from {}", url);

URLDownload download = new URLDownload(url);
download.canBeReached();

String json = download.asString();
Matcher matches = JSON_CONTAINING_URL_PATTERN.matcher(json);

if (matches.find()) {
String coverUrlString = matches.group(1);
if (coverUrlString != null) {
return coverUrlString;
}
}
} catch (MalformedURLException | FetcherException e) {
LOGGER.error("Error while querying cover url, using fallback", e);
}
}
return "https://covers.openlibrary.org/b/isbn/" + isbn.asString() + "-L.jpg";
}

private static Optional<LinkedFile> tryToDownloadLinkedFile(ExternalApplicationsPreferences externalApplicationsPreferences, Path directory, String url, String location, String name) {
final Path subdirectory = directory.resolve(location);

subdirectory.toFile().mkdirs();
if (subdirectory.toFile().exists()) {
final Optional<String> extension = FileUtil.getFileExtension(FileUtil.getFileNameFromUrl(url));
final Path destination = subdirectory.resolve(extension.map(x -> name + "." + x).orElse(name));
final String link = directory.relativize(destination).toString();

if (destination.toFile().exists()) {
return Optional.of(new LinkedFile("[cover]", link, inferFileTypeFromExtension(externalApplicationsPreferences, extension), url));

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

URLDownload download = new URLDownload(url);
download.canBeReached();

final String type = inferFileType(externalApplicationsPreferences, download.getMimeType(), extension);
download.toFile(destination);
return Optional.of(new LinkedFile("[cover]", link, type, url));

} catch (UnirestException | FetcherException | MalformedURLException e) {
LOGGER.error("Error while downloading cover image file", e);
}
} else {
LOGGER.warn("File directory not available while downloading cover image {}. Storing as URL in file field.", url);
return Optional.of(new LinkedFile("[cover]", url, ""));
}

return Optional.empty();
}

private static String inferFileType(ExternalApplicationsPreferences externalApplicationsPreferences, Optional<String> mime, Optional<String> extension) {
if (mime.isPresent()) {
Optional<ExternalFileType> suggested = ExternalFileTypes.getExternalFileTypeByMimeType(mime.get(), externalApplicationsPreferences);
if (suggested.isPresent()) {
return suggested.get().getName();
}
}
return inferFileTypeFromExtension(externalApplicationsPreferences, extension);
}

private static String inferFileTypeFromExtension(ExternalApplicationsPreferences externalApplicationsPreferences, Optional<String> extension) {
return extension.map(x -> ExternalFileTypes.getExternalFileTypeByExt(x, externalApplicationsPreferences).map(t -> t.getName()).orElse("")).orElse("");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.logic.ai.AiService;
import org.jabref.gui.importer.BookCoverFetcher;
import org.jabref.logic.importer.CompositeIdFetcher;
import org.jabref.logic.importer.FetcherClientException;
import org.jabref.logic.importer.FetcherException;
Expand Down Expand Up @@ -234,6 +235,10 @@ public StringProperty bibtexTextProperty() {
public ReadOnlyBooleanProperty bibtexTextValidatorProperty() {
return bibtexTextValidator.getValidationStatus().validProperty();
}

private Optional<BibEntry> withCoversAttached(Optional<BibEntry> entry) {
return BookCoverFetcher.withAttachedCoverFileIfExists(entry, libraryTab.getBibDatabaseContext(), preferences.getFilePreferences(), preferences.getExternalApplicationsPreferences());
}

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

return fetcher.performSearchById(text);
return withCoversAttached(fetcher.performSearchById(text));
}
}

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

return fetcher.performSearchById(text);
return withCoversAttached(fetcher.performSearchById(text));
}
}

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

import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.StandardActions;
Expand All @@ -25,6 +26,11 @@ public class LinkedFilesTab extends AbstractPreferenceTabView<LinkedFilesTabView

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

@FXML private RadioButton 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;
}
}

61 changes: 59 additions & 2 deletions jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
Expand All @@ -25,14 +27,19 @@
import org.jabref.gui.theme.ThemeManager;
import org.jabref.gui.util.UiTaskExecutor;
import org.jabref.gui.util.WebViewStore;
import org.jabref.gui.externalfiletype.ExternalFileType;
import org.jabref.gui.externalfiletype.ExternalFileTypes;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.layout.format.Number;
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 @@ -222,17 +229,67 @@ private String formatError(BibEntry entry, Throwable exception) {
}

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

layoutText = """
<html>
<body id="previewBody">
<div id="content"> %s </div>
%s <div id="content"> %s </div>
</body>
</html>
""".formatted(text);
""".formatted(coverIfAny, text);
highlightLayoutText();
setHvalue(0);
}

private Optional<String> getCoverImageURI() {
if (shouldShowCoverImage()) {
String nameFromFormat = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileNamePattern()).orElse("cover");

List<LinkedFile> linkedFiles = entry.getFiles();
for (LinkedFile file : linkedFiles) {
// matches images that are either named according to the preferred file name format
// or images with case-insensitive "[cover]" in their description, to allow using any image regardless of name

if (file.getDescription().toLowerCase().contains("[cover]") || isFileTypeAValidCoverImage(file.getFileType()) && (FileUtil.getBaseName(file.getFileName()).equals(nameFromFormat))) {
Copy link
Member

Choose a reason for hiding this comment

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

Tags are normally starting with #. I would prefer #cover. WDYT @InAnYan ?

return file.getURI(databaseContext, preferences.getFilePreferences());
}
}
}
return Optional.empty();
}

private boolean shouldShowCoverImage() {
//entry is sometimes null when setPreviewText is called
if (entry == null) {
return false;
}

return switch (entry.getType()) {
case StandardEntryType.Book, StandardEntryType.Booklet, StandardEntryType.BookInBook, StandardEntryType.InBook, StandardEntryType.MvBook -> true;
default -> false;
};
}

private boolean isFileTypeAValidCoverImage(String fileType) {
// to allow url links
if (fileType.equals("")) {
return true;
}

// needed because type names are stored in a localization dependent way
Optional<ExternalFileType> actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences());

if (actualFileType.isPresent()) {
return actualFileType.get().getMimeType().startsWith("image/");
}
return false;
}

private void highlightLayoutText() {
if (layoutText == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@
text="%When downloading files, or moving linked files to the file directory, use the bib file location."/>
</tooltip>
</RadioButton>

<RadioButton fx:id="bookCoverDownload" text="%Automatically download cover images"/>
<HBox alignment="CENTER_LEFT" spacing="10.0">
<Label fx:id="bookCoverLabel" text="%Cover image download location"/>
<TextField fx:id="bookCoverLocation" HBox.hgrow="ALWAYS"/>
</HBox>

<Label styleClass="sectionHeader" text="%Open file explorer"/>
<RadioButton fx:id="openFileExplorerInFilesDirectory" text="%Open file explorer in files directory"
toggleGroup="$fileExplorerToggleGroup" selected="true"/>
Expand Down
Loading
Loading