Skip to content

Add support for book front covers, again#14777

Merged
koppor merged 162 commits intoJabRef:mainfrom
bblhd:feature/book-front-cover-10120
Jan 12, 2026
Merged

Add support for book front covers, again#14777
koppor merged 162 commits intoJabRef:mainfrom
bblhd:feature/book-front-cover-10120

Conversation

@bblhd
Copy link
Contributor

@bblhd bblhd commented Jan 2, 2026

User description

Closes #10120

This is a re-attempt of a previous PR #14330, which was reverted after merging due to failing tests on windows.

This pull request adds support for cover images which can display in the preview of entries. It also allows new entries to automatically download cover images from either "https://bookcover.longitood.com" or "https://covers.openlibrary.org", and preferences to control downloading behaviour. Key changes are:

  • The ability for PreviewViewer to display images
  • A new BookCoverFetcher class to download cover images
  • Hooks in NewEntryViewModel to do cover fetching
  • New options in FilePreferences and LinkedFilesTab

Steps to test

  1. Add a new entry using the ISBN "978-1449310509", if you open the preview for the new entry, the cover image should be visible.
  2. Under File > Preferences, in the Entry Preview tab, disable the "Download cover images" setting.
  3. Add a new entry using the ISBN "978-1449373320", there should be no cover image if you open the preview.

Mandatory checks

Images

Shows the appearance of book covers in the citation preview. Shows the appearance of the book cover checkbox in preferences.

PR Type

Enhancement


Description

  • Add book cover image support for entry previews

  • Implement automatic cover downloading from ISBN via two APIs

  • Add user preference to control cover image downloading behavior

  • Refactor file utility methods for better URL and filename handling


Diagram Walkthrough

flowchart LR
  A["Entry with ISBN"] -->|BookCoverFetcher| B["Download Cover Image"]
  B -->|from bookcover.longitood.com| C["Cover File"]
  B -->|fallback to covers.openlibrary.org| C
  C -->|PreviewViewer| D["Display in Preview"]
  E["User Preference"] -->|shouldDownloadCovers| B
Loading

File Walkthrough

Relevant files
Enhancement
17 files
BookCoverFetcher.java
New class for downloading book cover images                           
+135/-0 
PreviewViewer.java
Add cover image display in preview panel                                 
+38/-23 
NewEntryViewModel.java
Integrate cover fetching into entry creation                         
+17/-2   
PreviewTab.java
Add UI checkbox for cover download preference                       
+3/-0     
PreviewTabViewModel.java
Add property binding for cover download setting                   
+10/-0   
PreviewPreferences.java
Add cover download preference property                                     
+17/-1   
ExternalFileType.java
Add NullMarked annotation for null safety                               
+3/-0     
Directories.java
Add method to get cover image directory                                   
+9/-0     
FileUtil.java
Refactor filename extraction from URLs                                     
+81/-20 
LinkedFile.java
Add method to extract filename from link                                 
+21/-0   
LinkedFileHandler.java
Simplify filename suggestion logic                                             
+27/-36 
PushToApplicationDetector.java
Use Files API instead of File.exists                                         
+3/-3     
ParseLatexDialogViewModel.java
Use Files API for directory checks                                             
+5/-6     
FileNodeViewModel.java
Use Files API for directory checks                                             
+2/-2     
PushApplicationDialogViewModel.java
Use Files API for path existence check                                     
+2/-1     
ThemeDialogViewModel.java
Use Files API for path existence check                                     
+2/-2     
PreviewTab.fxml
Add checkbox UI for cover download preference                       
+3/-0     
Configuration changes
2 files
JabRefGuiPreferences.java
Add preference storage for cover image download                   
+7/-1     
check-links.yml
Add permissions for workflow execution                                     
+4/-0     
Bug fix
2 files
ImportHandler.java
Use FileUtil for URL filename extraction                                 
+5/-2     
URLUtil.java
Remove duplicate URL filename extraction                                 
+0/-16   
Tests
8 files
AutoRenameFileOnEntryChangeTest.java
Add helper method for file existence assertions                   
+26/-17 
AbstractJabKitTest.java
Add file existence assertion helper methods                           
+18/-3   
ConvertTest.java
Use new file assertion helper methods                                       
+5/-6     
JabKitTest.java
Use new file assertion helper methods                                       
+3/-3     
PseudonymizeTest.java
Use new file assertion helper methods                                       
+8/-9     
SearchTest.java
Use new file assertion helper methods                                       
+2/-2     
LinkedFileHandlerTest.java
Expand test coverage for filename suggestions                       
+152/-40
FileUtilTest.java
Add comprehensive URL filename extraction tests                   
+95/-49 
Documentation
4 files
JabRef_en.properties
Add localization for cover download option                             
+2/-0     
CHANGELOG.md
Document book cover image feature addition                             
+1/-0     
PRIVACY.md
Add privacy policies for cover image APIs                               
+43/-41 
0040-display-front-cover-in-preview-tab.md
Update image paths to assets directory                                     
+4/-4     
Additional files
1 files
FileNameUniqueness.java +1/-3     

bblhd and others added 30 commits October 17, 2025 03:43
return Optional.empty();
}
String realFileNameString = realFileName.toString();
String extension = FilenameUtils.getExtension(realFileNameString);
Copy link
Contributor Author

@bblhd bblhd Jan 7, 2026

Choose a reason for hiding this comment

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

I think we should use our own implementation here for getFileExtension and getBaseName, because other implementations will have different goals and behaviour, and then we have control over how it works.

Specifically:

  • FilenameUtils.getExtension doesn't trim whitspace or convert to lowercase ("file.PdF " gives "PdF " instead of "pdf")
  • FilenameUtils.getBaseName appears to use both windows and unix path seperators (a/b\c" gives "c" on all operating systems instead of either "b\c" or "c")
  • FilenameUtils.getBaseName treats files starting with a dot as extensions (".name" gives "" instead of ".name")

Alternatively, we could use FilenameUtils and just explicity cover its edge cases, though this may be hard for the path seperator issue.

Copy link
Member

Choose a reason for hiding this comment

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

I am not sure about the casings - lets see, what happens. Maybe, you see the consequences.

b\c is a fucked-up filename.

FilenameUtils.getBaseName is patched to reteurn .tmp for .tmp . See tetts for .StartsWithADotIsNotAnExtension

LOGGER.warn("Was not a valid URL {}", link, e);
return Optional.empty();
}
Path fileName = Path.of(uri.getPath()).getFileName();
Copy link
Contributor Author

@bblhd bblhd Jan 7, 2026

Choose a reason for hiding this comment

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

Path.of(uri.getPath()).getFileName() will get the directory if there is no file name, instead of a null or empty string ("https://www.example.com/path/to/" gives "to")

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 intended. OK @Siedlerchr ?

@koppor koppor removed the status: awaiting-second-review For non-trivial changes label Jan 12, 2026
@github-actions github-actions bot added status: changes-required Pull requests that are not yet complete and removed status: changes-required Pull requests that are not yet complete labels Jan 12, 2026
@github-actions github-actions bot removed the status: changes-required Pull requests that are not yet complete label Jan 12, 2026
@koppor koppor marked this pull request as ready for review January 12, 2026 21:12
@qodo-free-for-open-source-projects
Copy link
Contributor

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
Unvalidated external downloads

Description: The code downloads cover images from external URLs without validating the content type or
size before writing to disk, potentially allowing malicious actors to cause denial of
service through resource exhaustion or store malicious content.
BookCoverFetcher.java [69-76]

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

URLDownload download;
try {
    download = new URLDownload(url);
} catch (MalformedURLException e) {
    LOGGER.error("Error while downloading cover image file", e);
    return;
Unvalidated URL construction

Description: The getImageUrl method constructs URLs using ISBN values and downloads content from
external services without proper validation, potentially exposing users to server-side
request forgery or malicious redirects.
BookCoverFetcher.java [113-133]

Referred Code
private static String getImageUrl(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;
Potential HTML injection

Description: The code retrieves cover image URLs and embeds them directly into HTML without
sanitization, potentially allowing HTML injection if the file path contains malicious
content or special characters.
PreviewViewer.java [253-258]

Referred Code
private Optional<String> getCoverImageURL() {
    if (entry != null) {
        return bookCoverFetcher.getDownloadedCoverForEntry(entry).map(path -> path.toUri().toString());
    }
    return Optional.empty();
}
Path traversal vulnerability

Description: The getFileNameFromUrl method processes URLs and extracts filenames without validating
against path traversal attacks, potentially allowing malicious URLs to specify paths
outside intended directories.
FileUtil.java [147-175]

Referred Code
public static Optional<String> getFileNameFromUrl(@NonNull String link) {
    // Apache Commons IO has no good support; FilenameUtils.getName(link) doesn't strip query parameters
    // Source: https://stackoverflow.com/a/33871029/873282
    URI uri;
    try {
        uri = new URI(link);
    } catch (URISyntaxException e) {
        LOGGER.warn("Was not a valid URL {}", link, e);
        return Optional.empty();
    }
    String pathFragment = uri.getPath();
    Path path;
    try {
        path = Path.of(pathFragment);
    } catch (InvalidPathException e) {
        // Try to keep something of the invalid path fragment
        return Optional.of(FileNameCleaner.cleanFileName(pathFragment));
    }
    Path fileName = path.getFileName();
    if (fileName == null) {
        // Happens if there is no path, e.g., at https://example.com/


 ... (clipped 8 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
URL validation concern: The code downloads cover images from external URLs without apparent validation of the URL
source or content type verification before download, which could pose security risks.

Referred Code
    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;

Learn more about managing compliance generic rules or creating your own custom rules

Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link
Contributor

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent double file extensions in suggested filenames

Modify getSuggestedFileName to prevent creating filenames with double extensions
by removing any existing extension from the generated name before appending the
new one.

jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java [263-270]

 public String getSuggestedFileName(@NonNull String extension) {
     assert !StringUtil.isBlank(extension);
     String filename = linkedFile.getFileName().orElse("file");
     final String targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern())
                                           .orElse(FileUtil.getBaseName(filename));
 
-    return FileUtil.getValidFileName(targetFileName + "." + extension);
+    return FileUtil.getValidFileName(FileUtil.getBaseName(targetFileName) + "." + extension);
 }
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: The suggestion identifies a valid bug where a filename pattern including an extension would lead to a double extension (e.g., name.pdf.pdf). This is a significant correctness issue in file naming logic.

Medium
Handle empty filenames after cleaning

In createFileNameFromPattern, add a check for an empty targetName after cleaning
illegal characters to prevent returning an empty filename, and fall back to the
citation key if necessary.

jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java [411-425]

 public static Optional<String> createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern) {
     String targetName = BracketedPattern.expandBrackets(fileNamePattern, ';', entry, database).trim();
 
     if (targetName.isEmpty() || "-".equals(targetName)) {
         return entry.getCitationKey().map(FileNameCleaner::cleanFileName);
     }
 
     // Remove LaTeX commands (e.g., \mkbibquote{}) from expanded fields before cleaning filename
     // See: https://github.com/JabRef/jabref/issues/12188
     targetName = REMOVE_LATEX_COMMANDS_FORMATTER.format(targetName);
     // Removes illegal characters from filename
     targetName = FileNameCleaner.cleanFileName(targetName);
 
+    if (targetName.isEmpty()) {
+        return entry.getCitationKey().map(FileNameCleaner::cleanFileName);
+    }
+
     return Optional.of(targetName);
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies that targetName could become empty after cleaning, and the current implementation would return an Optional.of(""). Adding a check after cleaning makes the method more robust by falling back to the citation key.

Medium
General
Use primary cover fetcher for all ISBN types

Update getImageUrl to query the primary cover fetching service for both ISBN-10
and ISBN-13 numbers, not just ISBN-13, before resorting to the fallback URL.

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java [113-134]

 private static String getImageUrl(ISBN isbn) {
-    if (isbn.isIsbn13()) {
-        String url = URL_FETCHER_URL + isbn.asString();
-        try {
-            LOGGER.info("Downloading book cover url from {}", url);
+    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);
+        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;
-                }
+        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);
         }
+    } catch (FetcherException | MalformedURLException e) {
+        LOGGER.error("Error while querying cover url, using fallback", e);
     }
     return IMAGE_FALLBACK_URL + isbn.asString() + IMAGE_FALLBACK_SUFFIX;
 }
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly points out that the primary cover fetcher is only used for ISBN-13, while it could also support ISBN-10. Applying this change would improve the cover fetching logic by trying the preferred service for all ISBN types.

Medium
  • More

@koppor koppor enabled auto-merge January 12, 2026 21:17
Copy link
Member

@koppor koppor left a comment

Choose a reason for hiding this comment

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

I think, we should take the risk of merging.

One follow-up issue created: #14848

@koppor koppor added this pull request to the merge queue Jan 12, 2026
Merged via the queue into JabRef:main with commit bd8b1b7 Jan 12, 2026
61 of 62 checks passed
Siedlerchr added a commit to st-rm-ng/jabref that referenced this pull request Jan 17, 2026
* upstream/main: (64 commits)
  New Crowdin updates (JabRef#14862)
  Make JDK25 available (JabRef#14861)
  Fix empty entries array when exporting group chat to JSON (JabRef#14814)
  feat: add right-click copy context menu to AI chat messages (JabRef#14722)
  FIX : generic error dialog and icon in Source Tab parsing (JabRef#14828)
  Factor out setup-* actions (JabRef#14859)
  Link .http files.
  Update dependency org.postgresql:postgresql to v42.7.9 (JabRef#14857)
  Add more commands to JabSrv (JabRef#14855)
  Fix JabRef#14821: Hide identifier action buttons when field is empty (JabRef#14831)
  Add GH_TOKEN to closed issues/PRs processing step
  New Crowdin updates (JabRef#14854)
  New Crowdin updates (JabRef#14849)
  Chore(deps): Bump jablib/src/main/resources/csl-styles from `0201999` to `f345aa8` (JabRef#14833)
  Add support for book front covers, again (JabRef#14777)
  Readd min width to button in new enty dialog (JabRef#14791)
  Replace plugin impl from jbang plugin (JabRef#14846)
  Revise AI policy wording
  Chore(deps): Bump jablib/src/main/resources/csl-locales (JabRef#14677)
  Update dependency com.konghq:unirest-modules-gson to v4.7.1 (JabRef#14845)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feature request: book front cover feature

5 participants