diff --git a/CHANGELOG.md b/CHANGELOG.md index adf7c7074ea..9c858e68928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - The export formats `listrefs`, `tablerefs`, `tablerefsabsbib`, now use the ISO date format in the footer [#10383](https://github.com/JabRef/jabref/pull/10383). - When searching for an identifier in the "Web search", the title of the search window is now "Identifier-based Web Search". [#10391](https://github.com/JabRef/jabref/pull/10391) - The ampersand checker now skips verbatim fields (`file`, `url`, ...). [#10419](https://github.com/JabRef/jabref/pull/10419) +- If no existing document is selected for exporting "XMP annotated pdf" JabRef will now create a new PDF file with a sample text and the metadata. [#10102](https://github.com/JabRef/jabref/issues/10102) +- We modified the DOI cleanup to infer the DOI from an ArXiV ID if it's present. [10426](https://github.com/JabRef/jabref/issues/10426) ### Fixed @@ -39,6 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - Biblatex's `journaltitle` is now also respected for showing the journal information. [#10397](https://github.com/JabRef/jabref/issues/10397) - JabRef does not hang anymore when exporting via CLI. [#10380](https://github.com/JabRef/jabref/issues/10380) - We fixed an issue where it was not possible to save a library on a network share under macOS due to an exception when acquiring a file lock [#10452](https://github.com/JabRef/jabref/issues/10452) +- We fixed an issue where exporting "XMP annotated pdf" without selecting an existing document would produce an exception. [#10102](https://github.com/JabRef/jabref/issues/10102) ### Removed diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java index 9833e46049c..05beb9a51f7 100644 --- a/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -277,6 +277,7 @@ public enum JabRefIcons implements JabRefIcon { APPLICATION_VIM(JabRefMaterialDesignIcon.VIM), APPLICATION_WINEDT(JabRefMaterialDesignIcon.WINEDT), APPLICATION_SUBLIMETEXT(JabRefMaterialDesignIcon.SUBLIME_TEXT), + APPLICATION_TEXSHOP(JabRefMaterialDesignIcon.TEXSHOP), KEY_BINDINGS(MaterialDesignK.KEYBOARD), FIND_DUPLICATES(MaterialDesignC.CODE_EQUAL), CONNECT_DB(MaterialDesignC.CLOUD_UPLOAD), @@ -351,7 +352,6 @@ public enum JabRefIcons implements JabRefIcon { ACCEPT_LEFT(MaterialDesignS.SUBDIRECTORY_ARROW_LEFT), ACCEPT_RIGHT(MaterialDesignS.SUBDIRECTORY_ARROW_RIGHT), MERGE_GROUPS(MaterialDesignS.SOURCE_MERGE); - private final JabRefIcon icon; JabRefIcons(Ikon... icons) { diff --git a/src/main/java/org/jabref/gui/icon/JabRefMaterialDesignIcon.java b/src/main/java/org/jabref/gui/icon/JabRefMaterialDesignIcon.java index e6b63d609db..6f697cc1e4a 100644 --- a/src/main/java/org/jabref/gui/icon/JabRefMaterialDesignIcon.java +++ b/src/main/java/org/jabref/gui/icon/JabRefMaterialDesignIcon.java @@ -30,7 +30,8 @@ public enum JabRefMaterialDesignIcon implements Ikon { SET_ALL("jab-setall", '\ue90c'), VSCODE("jab-vsvode", '\ue90d'), CANCEL("jab-cancel", '\ue90e'), - SUBLIME_TEXT("jab-sublime-text", '\ue90f'); + SUBLIME_TEXT("jab-sublime-text", '\ue90f'), + TEXSHOP("jab-texshop", '\ue910'); private String description; private int code; diff --git a/src/main/java/org/jabref/gui/push/PushToTexShop.java b/src/main/java/org/jabref/gui/push/PushToTexShop.java index 24fadcf1b8f..72f8c66a7a1 100644 --- a/src/main/java/org/jabref/gui/push/PushToTexShop.java +++ b/src/main/java/org/jabref/gui/push/PushToTexShop.java @@ -34,7 +34,7 @@ public String getDisplayName() { @Override public JabRefIcon getApplicationIcon() { - return IconTheme.JabRefIcons.APPLICATION_GENERIC; + return IconTheme.JabRefIcons.APPLICATION_TEXSHOP; } @Override diff --git a/src/main/java/org/jabref/logic/cleanup/DoiCleanup.java b/src/main/java/org/jabref/logic/cleanup/DoiCleanup.java index 3657acabb5f..1cb3f2d04e3 100644 --- a/src/main/java/org/jabref/logic/cleanup/DoiCleanup.java +++ b/src/main/java/org/jabref/logic/cleanup/DoiCleanup.java @@ -13,17 +13,19 @@ import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.field.UnknownField; +import org.jabref.model.entry.identifier.ArXivIdentifier; import org.jabref.model.entry.identifier.DOI; /** - * Formats the DOI (e.g. removes http part) and also moves DOIs from note, url or ee field to the doi field. + * Formats the DOI (e.g. removes http part) and also infers DOIs from the note, url, eprint or ee fields. */ public class DoiCleanup implements CleanupJob { /** * Fields to check for DOIs. */ - private static final List FIELDS = Arrays.asList(StandardField.NOTE, StandardField.URL, new UnknownField("ee")); + private static final List FIELDS = Arrays.asList(StandardField.NOTE, StandardField.URL, StandardField.EPRINT, + new UnknownField("ee")); @Override public List cleanup(BibEntry entry) { @@ -57,7 +59,9 @@ public List cleanup(BibEntry entry) { } else { // As the Doi field is empty we now check if note, url, or ee field contains a Doi for (Field field : FIELDS) { - Optional doi = entry.getField(field).flatMap(DOI::parse); + Optional fieldContentOpt = entry.getField(field); + + Optional doi = fieldContentOpt.flatMap(DOI::parse); if (doi.isPresent()) { // Update Doi @@ -65,6 +69,15 @@ public List cleanup(BibEntry entry) { change.ifPresent(changes::add); removeFieldValue(entry, field, changes); } + + if (StandardField.EPRINT == field) { + fieldContentOpt.flatMap(ArXivIdentifier::parse) + .flatMap(ArXivIdentifier::inferDOI) + .ifPresent(inferredDoi -> { + Optional change = entry.setField(StandardField.DOI, inferredDoi.getDOI()); + change.ifPresent(changes::add); + }); + } } } return changes; diff --git a/src/main/java/org/jabref/logic/exporter/XmpPdfExporter.java b/src/main/java/org/jabref/logic/exporter/XmpPdfExporter.java index f6ece8646c2..21e671b7d4c 100644 --- a/src/main/java/org/jabref/logic/exporter/XmpPdfExporter.java +++ b/src/main/java/org/jabref/logic/exporter/XmpPdfExporter.java @@ -1,5 +1,7 @@ package org.jabref.logic.exporter; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Objects; @@ -11,6 +13,12 @@ import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; + public class XmpPdfExporter extends Exporter { private final XmpPreferences xmpPreferences; @@ -26,7 +34,24 @@ public void export(BibDatabaseContext databaseContext, Path pdfFile, List getClassification() { } } + /** + * ArXiV articles are assigned DOIs automatically, which starts with a DOI prefix '10.48550/' followed by the ArXiV + * ID (replacing the colon with a period). + *

+ * For more information: + * + * new-arxiv-articles-are-now-automatically-assigned-dois + * */ + public Optional inferDOI() { + if (StringUtil.isBlank(identifier)) { + return Optional.empty(); + } + + return DOI.parse("10.48550/arxiv." + identifier); + } + @Override public String toString() { return "ArXivIdentifier{" + diff --git a/src/main/resources/fonts/JabRefMaterialDesign.ttf b/src/main/resources/fonts/JabRefMaterialDesign.ttf index 769fc1fe9e5..1dad4686d10 100644 Binary files a/src/main/resources/fonts/JabRefMaterialDesign.ttf and b/src/main/resources/fonts/JabRefMaterialDesign.ttf differ diff --git a/src/test/java/org/jabref/logic/exporter/XmpPdfExporterTest.java b/src/test/java/org/jabref/logic/exporter/XmpPdfExporterTest.java index 3e07fd1f085..afa5b3dad60 100644 --- a/src/test/java/org/jabref/logic/exporter/XmpPdfExporterTest.java +++ b/src/test/java/org/jabref/logic/exporter/XmpPdfExporterTest.java @@ -7,9 +7,15 @@ import java.util.stream.Stream; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import org.jabref.logic.cleanup.FieldFormatterCleanup; +import org.jabref.logic.formatter.bibtexfields.NormalizeNamesFormatter; +import org.jabref.logic.importer.ImportFormatPreferences; +import org.jabref.logic.importer.fileformat.PdfXmpImporter; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.xmp.XmpPreferences; +import org.jabref.logic.xmp.XmpUtilWriter; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -21,12 +27,18 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -41,6 +53,8 @@ class XmpPdfExporterTest { private static BibEntry vapnik2000 = new BibEntry(StandardEntryType.Article); private XmpPdfExporter exporter; + private PdfXmpImporter importer; + private XmpPreferences xmpPreferences; private BibDatabaseContext databaseContext; private JournalAbbreviationRepository abbreviationRepository; @@ -84,7 +98,7 @@ private static void initBibEntries() throws IOException { vapnik2000.setCitationKey("vapnik2000"); vapnik2000.setField(StandardField.TITLE, "The Nature of Statistical Learning Theory"); vapnik2000.setField(StandardField.PUBLISHER, "Springer Science + Business Media"); - vapnik2000.setField(StandardField.AUTHOR, "Vladimir N. Vapnik"); + vapnik2000.setField(StandardField.AUTHOR, "Vapnik, Vladimir N."); vapnik2000.setField(StandardField.DOI, "10.1007/978-1-4757-3264-1"); vapnik2000.setField(StandardField.OWNER, "Ich"); } @@ -99,9 +113,13 @@ void setUp() throws IOException { when(filePreferences.getUserAndHost()).thenReturn(tempDir.toAbsolutePath().toString()); when(filePreferences.shouldStoreFilesRelativeToBibFile()).thenReturn(false); - XmpPreferences xmpPreferences = new XmpPreferences(false, Collections.emptySet(), new SimpleObjectProperty<>(',')); + xmpPreferences = new XmpPreferences(false, Collections.emptySet(), new SimpleObjectProperty<>(',')); exporter = new XmpPdfExporter(xmpPreferences); + ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); + when(importFormatPreferences.fieldPreferences().getNonWrappableFields()).thenReturn(FXCollections.emptyObservableList()); + importer = new PdfXmpImporter(xmpPreferences); + databaseContext = new BibDatabaseContext(); BibDatabase dataBase = databaseContext.getDatabase(); @@ -111,6 +129,17 @@ void setUp() throws IOException { dataBase.insertEntry(vapnik2000); } + @AfterEach + void reset() throws IOException { + List expectedEntries = databaseContext.getEntries(); + for (BibEntry entry : expectedEntries) { + entry.clearField(StandardField.FILE); + } + LinkedFile linkedFile = createDefaultLinkedFile("existing.pdf", tempDir); + olly2018.setFiles(List.of(linkedFile)); + toral2006.setFiles(List.of(new LinkedFile("non-existing", "path/to/nowhere.pdf", "PDF"))); + } + @ParameterizedTest @MethodSource("provideBibEntriesWithValidPdfFileLinks") void successfulExportToAllFilesOfEntry(BibEntry bibEntryWithValidPdfFileLink) throws Exception { @@ -143,6 +172,39 @@ void unsuccessfulExportToFileByPath(Path path) throws Exception { assertFalse(exporter.exportToFileByPath(databaseContext, filePreferences, path, abbreviationRepository)); } + @ParameterizedTest + @MethodSource("providePathToNewPDFs") + public void testRoundtripExportImport(Path path) throws Exception { + try (PDDocument document = new PDDocument()) { + PDPage page = new PDPage(); + document.addPage(page); + + try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { + contentStream.beginText(); + contentStream.newLineAtOffset(25, 500); + contentStream.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12); + contentStream.showText("This PDF was created by JabRef. It demonstrates the embedding of XMP data in PDF files. Please open the file metadata view of your PDF viewer to see the attached files. Note that the normal usage is to embed the BibTeX data in an existing PDF."); + contentStream.endText(); + } + document.save(path.toString()); + } + new XmpUtilWriter(xmpPreferences).writeXmp(path, databaseContext.getEntries(), databaseContext.getDatabase()); + + List importedEntries = importer.importDatabase(path).getDatabase().getEntries(); + importedEntries.forEach(bibEntry -> new FieldFormatterCleanup(StandardField.AUTHOR, new NormalizeNamesFormatter()).cleanup(bibEntry)); + + List expectedEntries = databaseContext.getEntries(); + for (BibEntry entry : expectedEntries) { + entry.clearField(StandardField.FILE); + entry.addFile(createDefaultLinkedFile("original.pdf", tempDir)); + } + assertEquals(expectedEntries, importedEntries); + } + + public static Stream providePathToNewPDFs() { + return Stream.of(Arguments.of(tempDir.resolve("original.pdf").toAbsolutePath())); + } + public static Stream providePathsToValidPDFs() { return Stream.of(Arguments.of(tempDir.resolve("existing.pdf").toAbsolutePath())); } @@ -156,12 +218,16 @@ public static Stream providePathsToInvalidPDFs() throws IOException { } private static LinkedFile createDefaultLinkedFile(String fileName, Path tempDir) throws IOException { + return createDefaultLinkedFile("", fileName, tempDir); + } + + private static LinkedFile createDefaultLinkedFile(String description, String fileName, Path tempDir) throws IOException { Path pdfFile = tempDir.resolve(fileName); try (PDDocument pdf = new PDDocument()) { pdf.addPage(new PDPage()); pdf.save(pdfFile.toAbsolutePath().toString()); } - return new LinkedFile("A linked pdf", pdfFile, "PDF"); + return new LinkedFile("", pdfFile, "PDF"); } }