diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..13804d6 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build + +# Continuous integration: make sure PDF Compiler always compiles cleanly. +on: + push: + branches: [main, development] + pull_request: + branches: [main, development] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Compile + run: | + mkdir -p build/classes + javac -Xlint:all -cp libs/pdfbox-app-3.0.2.jar -d build/classes src/PDFCompiler.java diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b1eed73 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,74 @@ +name: Release + +# Automated releases: pushing a version tag (e.g. v3.2) compiles the program, +# bundles a self-contained runnable JAR (PDF Compiler + Apache PDFBox), and +# publishes it as a GitHub Release asset. +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to build (without the leading v), e.g. 3.2' + required: true + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Check out source + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Resolve version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION="${GITHUB_REF_NAME#v}" + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "Building PDF Compiler ${VERSION}" + + - name: Build self-contained runnable JAR + id: build + run: | + set -euo pipefail + VERSION="${{ steps.version.outputs.version }}" + ARTIFACT="PDF-Compiler-${VERSION}.jar" + + mkdir -p build/fatjar + javac -Xlint:all -cp libs/pdfbox-app-3.0.2.jar -d build/fatjar src/PDFCompiler.java + + # Unpack PDFBox into the same tree, then repack everything with our + # own manifest so the result is a double-clickable / runnable JAR. + ( cd build/fatjar && unzip -oq ../../libs/pdfbox-app-3.0.2.jar ) + rm -f build/fatjar/META-INF/MANIFEST.MF + + jar --create --file "build/${ARTIFACT}" --main-class PDFCompiler -C build/fatjar . + + # Sanity checks (no GUI launch: this is a headless runner). + # Confirm our entry point is the manifest Main-Class and is bundled. + unzip -p "build/${ARTIFACT}" META-INF/MANIFEST.MF | grep -q '^Main-Class: PDFCompiler' + unzip -l "build/${ARTIFACT}" | grep -q 'PDFCompiler.class' + unzip -l "build/${ARTIFACT}" | grep -q 'org/apache/pdfbox/multipdf/PDFMergerUtility.class' + + echo "artifact=build/${ARTIFACT}" >> "$GITHUB_OUTPUT" + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: PDF Compiler ${{ steps.version.outputs.version }} + tag_name: v${{ steps.version.outputs.version }} + generate_release_notes: true + files: ${{ steps.build.outputs.artifact }} diff --git a/.gitignore b/.gitignore index 2633867..00f92fa 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ out/ .DS_Store -.github \ No newline at end of file +.claude/ +build/ +*.class diff --git a/README.md b/README.md index f33457c..17b7bd8 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PDF Compiler version 3.1 +# PDF Compiler version 3.2 A Java program (that uses AWT/Swing as the GUI) that compiles multiple PDF files into one PDF. We have rewritten the whole code base from our Python source code to Java, for the application to become operating system independent and easier to create binaries for all platforms. @@ -28,6 +28,12 @@ from previous versions may not be working due to the rewrite, but will be implem ### Release 3.1 (Part of Spring 2024) Rewrote drag-n-drop in Java for PDF Compiler to easily drag and drop files. Squashed some bugs. -(c) 2023 - 2024 Morales Research Inc +### Release 3.2 (2026) +Sets the Nimbus look and feel as the default UI theme for a consistent, modern appearance across Windows, macOS, and Linux. +General codebase cleanup (lambda event handlers, shared drag-and-drop/file-chooser logic, case-insensitive `.pdf` +matching, and consolidated error handling). Adds automated GitHub Actions release builds that compile a runnable, +self-contained JAR and publish it as a release asset on every `v*` tag. + +(c) 2023 - 2026 Morales Research Inc (c) 2023 - 2025 The University of Texas at Austin, Department of Computer Science diff --git a/src/PDFCompiler.java b/src/PDFCompiler.java index b1ce294..cca51c3 100644 --- a/src/PDFCompiler.java +++ b/src/PDFCompiler.java @@ -64,17 +64,11 @@ The Open Research License (ORL) is a permissive open source license designed to import javax.swing.*; import java.awt.*; import java.awt.datatransfer.DataFlavor; -import java.awt.desktop.AboutHandler; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; -import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDropEvent; -import java.awt.dnd.DropTargetEvent; -import java.awt.dnd.DropTargetListener; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -86,9 +80,11 @@ The Open Research License (ORL) is a permissive open source license designed to * program is to compress/combine PDFs into one. * * @author Abdon Morales (abdonm@cs.utexas.edu) - * @version 3.1 + * @version 3.2 */ public class PDFCompiler extends JFrame { + private static final long serialVersionUID = 1L; + private JList pdfList; private DefaultListModel pdfListModel; private JLabel resultLabel; @@ -111,7 +107,7 @@ public PDFCompiler() { * "actions"/clicks. */ private void initUI() { - setTitle("PDF Compiler 3.1"); + setTitle("PDF Compiler 3.2"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setSize(400, 300); @@ -127,29 +123,16 @@ private void initUI() { resultLabel = new JLabel(""); /* Adding drag -n- drop file support */ - new DropTarget(pdfList, new DropTargetListener() { - @Override - public void dragEnter(DropTargetDragEvent dtde) {} - - @Override - public void dragOver(DropTargetDragEvent dtde) {} - - @Override - public void dropActionChanged(DropTargetDragEvent dtde) {} - - @Override - public void dragExit(DropTargetEvent dte) {} - + new DropTarget(pdfList, new DropTargetAdapter() { @Override + @SuppressWarnings("unchecked") public void drop(DropTargetDropEvent dtde) { try { dtde.acceptDrop(DnDConstants.ACTION_COPY); - List droppedFiles = (List) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + List droppedFiles = (List) dtde.getTransferable() + .getTransferData(DataFlavor.javaFileListFlavor); for (File file : droppedFiles) { - if (file.getName().endsWith(".pdf")) { - pdfListModel.addElement(file.getName()); - pdfPaths.put(file.getName(), file); - } + addPdfFile(file); } } catch (Exception ex) { ex.printStackTrace(); @@ -167,22 +150,22 @@ public void drop(DropTargetDropEvent dtde) { contentPane.add(resultLabel, BorderLayout.NORTH); // Button actions - addButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - addPDFs(); - } - }); - combineButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - try { - combinePDFs(); - } catch (FileNotFoundException ex) { - throw new RuntimeException(ex); - } - } - }); + addButton.addActionListener(e -> addPDFs()); + combineButton.addActionListener(e -> combinePDFs()); + } + + /** + * Adds a single file to the list if it is a PDF. The check is + * case-insensitive so files ending in ".PDF" are accepted too. Shared by + * both the "Add PDFs" file chooser and the drag-and-drop handler. + * + * @param file the file to add + */ + private void addPdfFile(File file) { + if (file != null && file.getName().toLowerCase().endsWith(".pdf")) { + pdfListModel.addElement(file.getName()); + pdfPaths.put(file.getName(), file); + } } /** @@ -197,12 +180,8 @@ private void addPDFs() { int result = fileChooser.showOpenDialog(this); if (result == JFileChooser.APPROVE_OPTION) { - File[] selectedFiles = fileChooser.getSelectedFiles(); - for (File file : selectedFiles) { - if (file.getName().endsWith(".pdf")) { - pdfListModel.addElement(file.getName()); - pdfPaths.put(file.getName(), file); - } + for (File file : fileChooser.getSelectedFiles()) { + addPdfFile(file); } } } @@ -212,7 +191,7 @@ private void addPDFs() { * creates the literal logic for combining the PDF using an open-source Java library called * Apache pdfbox. [This method throws an exception if the PDF is not found in specified path.] */ - private void combinePDFs() throws FileNotFoundException { + private void combinePDFs() { List selectedFiles = new ArrayList<>(); for (int i = 0; i < pdfListModel.size(); i++) { String fileName = pdfListModel.getElementAt(i); @@ -228,26 +207,26 @@ private void combinePDFs() throws FileNotFoundException { fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); int result = fileChooser.showSaveDialog(this); - if (result == JFileChooser.APPROVE_OPTION) { - File outputFile = fileChooser.getSelectedFile(); - if (!outputFile.getName().endsWith(".pdf")) { - outputFile = new File(outputFile.getPath() + ".pdf"); - } + if (result != JFileChooser.APPROVE_OPTION) { + resultLabel.setText("PDF combining cancelled."); + return; + } - PDFMergerUtility pdfMerger = new PDFMergerUtility(); + File outputFile = fileChooser.getSelectedFile(); + if (!outputFile.getName().toLowerCase().endsWith(".pdf")) { + outputFile = new File(outputFile.getPath() + ".pdf"); + } + + PDFMergerUtility pdfMerger = new PDFMergerUtility(); + try { for (File file : selectedFiles) { pdfMerger.addSource(file); } pdfMerger.setDestinationFileName(outputFile.getPath()); - - try { - pdfMerger.mergeDocuments(null); - resultLabel.setText("Combined PDF saved as " + outputFile.getName()); - } catch (IOException e) { - resultLabel.setText("Error combining PDFs."); - } - } else { - resultLabel.setText("PDF combining cancelled."); + pdfMerger.mergeDocuments(null); + resultLabel.setText("Combined PDF saved as " + outputFile.getName()); + } catch (IOException e) { + resultLabel.setText("Error combining PDFs."); } } @@ -259,25 +238,20 @@ private void initMacOSAboutMenu() { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); if (desktop.isSupported(Desktop.Action.APP_ABOUT)) { - desktop.setAboutHandler(new AboutHandler() { - @Override - public void handleAbout(java.awt.desktop.AboutEvent e) { - JOptionPane.showMessageDialog( - null, - """ - PDF Compiler - Version 3.1 - Released May 16, 2024 - Copyright (C) 2024 Morales Research \ - Technology Inc - Copyright (C) 2023 - 24 The University of Texas at \ - Austin, - Department of Computer Science""", - "About PDF Compiler", - JOptionPane.INFORMATION_MESSAGE - ); - } - }); + desktop.setAboutHandler(e -> JOptionPane.showMessageDialog( + null, + """ + PDF Compiler + Version 3.2 + Released June 30, 2026 + Copyright (C) 2026 Morales Research \ + Technology Inc + Copyright (C) 2023 - 26 The University of Texas at \ + Austin, + Department of Computer Science""", + "About PDF Compiler", + JOptionPane.INFORMATION_MESSAGE + )); } } } @@ -288,9 +262,29 @@ public void handleAbout(java.awt.desktop.AboutEvent e) { */ public static void main(String[] args) { System.setProperty("apple.awt.application.name", "PDF Compiler"); + setNimbusLookAndFeel(); SwingUtilities.invokeLater(() -> { PDFCompiler pdfCompiler = new PDFCompiler(); pdfCompiler.setVisible(true); }); } + + /** + * Sets the Nimbus look and feel as the default user interface theme so the + * application has a consistent, modern appearance across platforms. If + * Nimbus is unavailable on the running JRE, the system silently falls back + * to the default cross-platform look and feel. + */ + private static void setNimbusLookAndFeel() { + try { + for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + if ("Nimbus".equals(info.getName())) { + UIManager.setLookAndFeel(info.getClassName()); + return; + } + } + } catch (ReflectiveOperationException | UnsupportedLookAndFeelException e) { + // Nimbus is not available; keep the default look and feel. + } + } }