Skip to content

Commit 30abcd4

Browse files
committed
Add support for viewing PDF files
1 parent a0edf8e commit 30abcd4

4 files changed

Lines changed: 184 additions & 15 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to this
8181

8282
### Acknowledgements
8383

84-
UView is built using these amazing open-source libraries:
84+
UView is built using a lot of amazing open-source libraries, including:
8585

8686
- [FlatLaf](https://www.formdev.com/flatlaf/) for the nice and modern UI.
8787
- [RSyntaxTextArea](https://bobbylight.github.io/RSyntaxTextArea/) for syntax highlighting.

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@
101101
<artifactId>imageio-tiff</artifactId>
102102
<version>3.10.1</version>
103103
</dependency>
104+
<dependency>
105+
<groupId>org.apache.pdfbox</groupId>
106+
<artifactId>pdfbox</artifactId>
107+
<version>3.0.2</version>
108+
</dependency>
104109
</dependencies>
105110

106111
<build>

src/main/java/io/github/pixelclover/uview/gui/AssetViewerFrame.java

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,28 @@
22

33
import io.github.pixelclover.uview.core.PackageManager;
44
import io.github.pixelclover.uview.model.UnityAsset;
5-
import java.awt.*;
5+
import java.awt.BorderLayout;
6+
import java.awt.Desktop;
7+
import java.awt.Dimension;
8+
import java.awt.FlowLayout;
9+
import java.awt.event.WindowAdapter;
10+
import java.awt.event.WindowEvent;
611
import java.io.File;
712
import java.io.IOException;
813
import java.nio.charset.StandardCharsets;
914
import java.nio.file.Files;
1015
import java.text.DecimalFormat;
1116
import java.util.Set;
12-
import javax.swing.*;
17+
import javax.swing.BorderFactory;
18+
import javax.swing.Box;
19+
import javax.swing.BoxLayout;
20+
import javax.swing.ImageIcon;
21+
import javax.swing.JButton;
22+
import javax.swing.JFrame;
23+
import javax.swing.JLabel;
24+
import javax.swing.JOptionPane;
25+
import javax.swing.JPanel;
26+
import javax.swing.JScrollPane;
1327

1428
public class AssetViewerFrame extends JFrame {
1529

@@ -41,10 +55,12 @@ public class AssetViewerFrame extends JFrame {
4155
Set.of(
4256
"png", "jpg", "jpeg", "gif", "tga", "bmp", "webp", "svg", "ico", "avif", "tiff", "tif");
4357
private static final Set<String> MEDIA_EXTENSIONS = Set.of("mp4", "mov", "wav", "mp3", "ogg");
58+
private static final Set<String> PDF_EXTENSIONS = Set.of("pdf");
4459
private static final DecimalFormat FILE_SIZE_FORMAT = new DecimalFormat("#,##0.0 KB");
4560

4661
private final PackageManager packageManager;
4762
private final Runnable onSaveCallback;
63+
private PdfViewerPanel pdfPanel;
4864

4965
public AssetViewerFrame(
5066
JFrame owner, UnityAsset asset, PackageManager packageManager, Runnable onSaveCallback) {
@@ -59,32 +75,43 @@ public AssetViewerFrame(
5975

6076
JPanel contentPanel = createContentPanel(asset);
6177
if (contentPanel == null) {
62-
dispose(); // Frame was closed by media handler
78+
dispose();
6379
return;
6480
}
6581

6682
add(contentPanel, BorderLayout.CENTER);
6783
add(createFooterPanel(asset), BorderLayout.SOUTH);
84+
85+
addWindowListener(
86+
new WindowAdapter() {
87+
@Override
88+
public void windowClosed(WindowEvent e) {
89+
if (pdfPanel != null) {
90+
try {
91+
pdfPanel.close();
92+
} catch (IOException ex) {
93+
System.err.println("Failed to close PDF document: " + ex.getMessage());
94+
}
95+
}
96+
}
97+
});
6898
}
6999

70100
private JPanel createFooterPanel(UnityAsset asset) {
71101
JPanel footer = new JPanel();
72102
footer.setLayout(new BoxLayout(footer, BoxLayout.X_AXIS));
73103
footer.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8));
74104

75-
// Path
76105
JLabel pathLabel = new JLabel(asset.assetPath());
77-
pathLabel.setToolTipText(asset.assetPath()); // Show full path on hover
106+
pathLabel.setToolTipText(asset.assetPath());
78107

79-
// Size
80108
String size = "N/A (Directory)";
81109
if (asset.content() != null) {
82110
double sizeInKb = asset.content().length / 1024.0;
83111
size = FILE_SIZE_FORMAT.format(sizeInKb);
84112
}
85113
JLabel sizeLabel = new JLabel(size);
86114

87-
// GUID
88115
JLabel guidLabel = new JLabel("GUID: " + asset.guid());
89116

90117
footer.add(pathLabel);
@@ -109,21 +136,29 @@ private JPanel createContentPanel(UnityAsset asset) {
109136
return createTextEditorPanel(asset);
110137
}
111138

112-
JPanel panel = new JPanel(new BorderLayout());
113-
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
139+
JPanel contentWrapperPanel = new JPanel(new BorderLayout());
114140

115141
if (IMAGE_EXTENSIONS.contains(extension)) {
116142
ImageIcon imageIcon = new ImageIcon(asset.content());
117143
JLabel imageLabel = new JLabel(imageIcon);
118-
panel.add(new JScrollPane(imageLabel), BorderLayout.CENTER);
144+
contentWrapperPanel.add(new JScrollPane(imageLabel), BorderLayout.CENTER);
145+
} else if (PDF_EXTENSIONS.contains(extension)) {
146+
try {
147+
this.pdfPanel = new PdfViewerPanel(asset.content());
148+
contentWrapperPanel.add(this.pdfPanel, BorderLayout.CENTER);
149+
} catch (IOException e) {
150+
JLabel errorLabel = new JLabel("Failed to load PDF: " + e.getMessage());
151+
errorLabel.setHorizontalAlignment(JLabel.CENTER);
152+
contentWrapperPanel.add(errorLabel, BorderLayout.CENTER);
153+
}
119154
} else if (MEDIA_EXTENSIONS.contains(extension)) {
120155
handleMediaAsset(asset);
121156
return null;
122157
} else {
123-
panel.add(new JLabel("Binary content cannot be previewed."), BorderLayout.CENTER);
158+
contentWrapperPanel.add(
159+
new JLabel("Binary content cannot be previewed."), BorderLayout.CENTER);
124160
}
125-
126-
return panel;
161+
return contentWrapperPanel;
127162
}
128163

129164
private JPanel createTextEditorPanel(UnityAsset asset) {
@@ -146,7 +181,7 @@ private JPanel createTextEditorPanel(UnityAsset asset) {
146181
e -> {
147182
byte[] newContent = syntaxTextPanel.getText().getBytes(StandardCharsets.UTF_8);
148183
packageManager.updateAssetContent(asset.assetPath(), newContent);
149-
syntaxTextPanel.markAsSaved(); // Reset the dirty state
184+
syntaxTextPanel.markAsSaved();
150185
onSaveCallback.run();
151186
});
152187

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package io.github.pixelclover.uview.gui;
2+
3+
import java.awt.BorderLayout;
4+
import java.awt.FlowLayout;
5+
import java.awt.event.KeyAdapter;
6+
import java.awt.event.KeyEvent;
7+
import java.awt.image.BufferedImage;
8+
import java.io.IOException;
9+
import javax.swing.BoxLayout;
10+
import javax.swing.ImageIcon;
11+
import javax.swing.JButton;
12+
import javax.swing.JLabel;
13+
import javax.swing.JOptionPane;
14+
import javax.swing.JPanel;
15+
import javax.swing.JScrollPane;
16+
import javax.swing.JTextField;
17+
import javax.swing.SwingUtilities;
18+
import org.apache.pdfbox.Loader;
19+
import org.apache.pdfbox.pdmodel.PDDocument;
20+
import org.apache.pdfbox.rendering.PDFRenderer;
21+
22+
/** A panel that displays a PDF document with page navigation controls. */
23+
public class PdfViewerPanel extends JPanel {
24+
25+
private final PDDocument document;
26+
private final PDFRenderer renderer;
27+
private int currentPage = 0;
28+
private final JLabel pageLabel = new JLabel();
29+
private final JButton prevButton = new JButton("< Prev");
30+
private final JButton nextButton = new JButton("Next >");
31+
private final JLabel statusLabel = new JLabel();
32+
private final JTextField pageInputField = new JTextField(4);
33+
34+
public PdfViewerPanel(byte[] pdfData) throws IOException {
35+
super(new BorderLayout());
36+
37+
// Load the document and create a renderer
38+
this.document = Loader.loadPDF(pdfData);
39+
this.renderer = new PDFRenderer(document);
40+
41+
// Main display area for the rendered page
42+
pageLabel.setHorizontalAlignment(JLabel.CENTER);
43+
add(new JScrollPane(pageLabel), BorderLayout.CENTER);
44+
45+
// Navigation and status controls panel
46+
JPanel controlsPanel = new JPanel();
47+
controlsPanel.setLayout(new BoxLayout(controlsPanel, BoxLayout.X_AXIS));
48+
49+
// Left-aligned navigation
50+
JPanel navPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
51+
navPanel.add(prevButton);
52+
navPanel.add(nextButton);
53+
54+
// Right-aligned page jump
55+
JPanel jumpPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
56+
jumpPanel.add(new JLabel("Go to page:"));
57+
jumpPanel.add(pageInputField);
58+
59+
controlsPanel.add(navPanel);
60+
controlsPanel.add(statusLabel);
61+
controlsPanel.add(jumpPanel);
62+
add(controlsPanel, BorderLayout.SOUTH);
63+
64+
setupActionListeners();
65+
updatePage(0); // Display the first page initially
66+
}
67+
68+
private void setupActionListeners() {
69+
prevButton.addActionListener(e -> updatePage(currentPage - 1));
70+
nextButton.addActionListener(e -> updatePage(currentPage + 1));
71+
72+
pageInputField.addKeyListener(
73+
new KeyAdapter() {
74+
@Override
75+
public void keyPressed(KeyEvent e) {
76+
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
77+
jumpToPage();
78+
}
79+
}
80+
});
81+
}
82+
83+
private void jumpToPage() {
84+
try {
85+
int pageNum = Integer.parseInt(pageInputField.getText());
86+
// Convert 1-based user input to 0-based index
87+
updatePage(pageNum - 1);
88+
} catch (NumberFormatException e) {
89+
JOptionPane.showMessageDialog(
90+
this, "Please enter a valid page number.", "Invalid Input", JOptionPane.ERROR_MESSAGE);
91+
}
92+
}
93+
94+
private void updatePage(int newPageIndex) {
95+
if (newPageIndex < 0 || newPageIndex >= document.getNumberOfPages()) {
96+
return; // Page index is out of bounds
97+
}
98+
currentPage = newPageIndex;
99+
100+
// Render the page in a background thread to keep the UI responsive
101+
SwingUtilities.invokeLater(
102+
() -> {
103+
try {
104+
BufferedImage image = renderer.renderImageWithDPI(currentPage, 150); // 150 DPI
105+
pageLabel.setIcon(new ImageIcon(image));
106+
statusLabel.setText("Page " + (currentPage + 1) + " of " + document.getNumberOfPages());
107+
pageInputField.setText(String.valueOf(currentPage + 1));
108+
109+
// Enable/disable navigation buttons
110+
prevButton.setEnabled(currentPage > 0);
111+
nextButton.setEnabled(currentPage < document.getNumberOfPages() - 1);
112+
} catch (IOException e) {
113+
pageLabel.setText("Failed to render page: " + e.getMessage());
114+
}
115+
});
116+
}
117+
118+
/**
119+
* Closes the underlying PDDocument to free up resources. This must be called when the panel is no
120+
* longer needed.
121+
*
122+
* @throws IOException if the document cannot be closed.
123+
*/
124+
public void close() throws IOException {
125+
if (document != null) {
126+
document.close();
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)