Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
out/
.DS_Store
.github
.claude/
build/
*.class
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
162 changes: 78 additions & 84 deletions src/PDFCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> pdfList;
private DefaultListModel<String> pdfListModel;
private JLabel resultLabel;
Expand All @@ -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);

Expand All @@ -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<File> droppedFiles = (List<File>) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
List<File> droppedFiles = (List<File>) 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();
Expand All @@ -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);
}
}

/**
Expand All @@ -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);
}
}
}
Expand All @@ -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<File> selectedFiles = new ArrayList<>();
for (int i = 0; i < pdfListModel.size(); i++) {
String fileName = pdfListModel.getElementAt(i);
Expand All @@ -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.");
}
}

Expand All @@ -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
));
}
}
}
Expand All @@ -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.
}
}
}
Loading