While many Java applications rely on external PDF viewers or browser components, there are scenarios where you need embedded, customizable PDF viewing capabilities. Apache PDFBox provides powerful tools not just for PDF creation and manipulation, but also for building custom PDF viewers. This article explores how to create a fully-featured PDF viewer using PDFBox, from basic rendering to advanced features like text selection and search.
Why Build a Custom PDF Viewer?
Common Use Cases:
- Embedded document viewing in enterprise applications
- Custom annotation and markup requirements
- Offline PDF viewing capabilities
- Branded document reader applications
- Specialized document processing workflows
Advantages of PDFBox:
- Pure Java solution (no native dependencies)
- Open source and actively maintained
- Extensive PDF manipulation capabilities
- Good performance for most use cases
- Customizable rendering pipeline
Setting Up Dependencies
Maven:
<dependencies> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox-tools</artifactId> <version>3.0.1</version> </dependency> </dependencies>
Gradle:
dependencies {
implementation 'org.apache.pdfbox:pdfbox:3.0.1'
implementation 'org.apache.pdfbox:pdfbox-tools:3.0.1'
}
Basic PDF Viewer Implementation
Let's start with a simple PDF viewer that displays one page at a time.
Basic PDF Viewer Class:
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class BasicPDFViewer extends JFrame {
private PDDocument document;
private PDFRenderer renderer;
private int currentPage = 0;
private JLabel imageLabel;
private JScrollPane scrollPane;
public BasicPDFViewer() {
initializeUI();
}
private void initializeUI() {
setTitle("PDF Viewer - Apache PDFBox");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(800, 1000);
setLocationRelativeTo(null);
// Create components
imageLabel = new JLabel();
imageLabel.setHorizontalAlignment(SwingConstants.CENTER);
scrollPane = new JScrollPane(imageLabel);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
add(scrollPane, BorderLayout.CENTER);
// Create toolbar
JToolBar toolBar = createToolbar();
add(toolBar, BorderLayout.NORTH);
// Create status bar
JLabel statusLabel = new JLabel("No document loaded");
add(statusLabel, BorderLayout.SOUTH);
}
private JToolBar createToolbar() {
JToolBar toolBar = new JToolBar();
JButton openButton = new JButton("Open PDF");
openButton.addActionListener(e -> openPDF());
JButton prevButton = new JButton("Previous");
prevButton.addActionListener(e -> showPreviousPage());
JButton nextButton = new JButton("Next");
nextButton.addActionListener(e -> showNextPage());
JButton zoomInButton = new JButton("Zoom In");
zoomInButton.addActionListener(e -> zoomIn());
JButton zoomOutButton = new JButton("Zoom Out");
zoomOutButton.addActionListener(e -> zoomOut());
toolBar.add(openButton);
toolBar.addSeparator();
toolBar.add(prevButton);
toolBar.add(nextButton);
toolBar.addSeparator();
toolBar.add(zoomInButton);
toolBar.add(zoomOutButton);
return toolBar;
}
public void openPDF() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter(
"PDF Files", "pdf"));
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
loadPDF(file);
}
}
private void loadPDF(File file) {
try {
// Close previous document
if (document != null) {
document.close();
}
// Load new document
document = PDDocument.load(file);
renderer = new PDFRenderer(document);
currentPage = 0;
setTitle("PDF Viewer - " + file.getName());
showPage(currentPage);
} catch (IOException ex) {
JOptionPane.showMessageDialog(this,
"Error loading PDF: " + ex.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
private void showPage(int pageNumber) {
if (document == null || pageNumber < 0 || pageNumber >= document.getNumberOfPages()) {
return;
}
try {
// Render page to image
BufferedImage image = renderer.renderImage(pageNumber, 1.0f);
ImageIcon icon = new ImageIcon(image);
imageLabel.setIcon(icon);
// Update status
updateStatus(pageNumber);
} catch (IOException ex) {
JOptionPane.showMessageDialog(this,
"Error rendering page: " + ex.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
private void showPreviousPage() {
if (currentPage > 0) {
currentPage--;
showPage(currentPage);
}
}
private void showNextPage() {
if (document != null && currentPage < document.getNumberOfPages() - 1) {
currentPage++;
showPage(currentPage);
}
}
private void zoomIn() {
// Zoom implementation would go here
JOptionPane.showMessageDialog(this, "Zoom In feature to be implemented");
}
private void zoomOut() {
// Zoom implementation would go here
JOptionPane.showMessageDialog(this, "Zoom Out feature to be implemented");
}
private void updateStatus(int pageNumber) {
int totalPages = document != null ? document.getNumberOfPages() : 0;
String status = String.format("Page %d of %d", pageNumber + 1, totalPages);
// Find and update status label
for (Component comp : getContentPane().getComponents()) {
if (comp instanceof JLabel && ((JLabel) comp).getText().contains("Page")) {
((JLabel) comp).setText(status);
break;
}
}
}
@Override
public void dispose() {
if (document != null) {
try {
document.close();
} catch (IOException e) {
e.printStackTrace();
}
}
super.dispose();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
new BasicPDFViewer().setVisible(true);
});
}
}
Advanced PDF Viewer with Zoom and Navigation
Let's enhance our viewer with better zoom capabilities and thumbnail navigation.
Enhanced PDF Viewer:
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class EnhancedPDFViewer extends JFrame {
private PDDocument document;
private PDFRenderer renderer;
private int currentPage = 0;
private float zoomFactor = 1.0f;
private JLabel imageLabel;
private JScrollPane scrollPane;
private JPanel thumbnailsPanel;
private List<JLabel> thumbnailLabels;
public EnhancedPDFViewer() {
initializeUI();
}
private void initializeUI() {
setTitle("Enhanced PDF Viewer");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1200, 800);
setLocationRelativeTo(null);
// Create main split pane
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
splitPane.setDividerLocation(200);
// Thumbnails panel
thumbnailsPanel = new JPanel();
thumbnailsPanel.setLayout(new BoxLayout(thumbnailsPanel, BoxLayout.Y_AXIS));
JScrollPane thumbnailsScroll = new JScrollPane(thumbnailsPanel);
thumbnailsScroll.setPreferredSize(new Dimension(180, 0));
// Main content area
imageLabel = new JLabel();
imageLabel.setHorizontalAlignment(SwingConstants.CENTER);
scrollPane = new JScrollPane(imageLabel);
splitPane.setLeftComponent(thumbnailsScroll);
splitPane.setRightComponent(scrollPane);
add(splitPane, BorderLayout.CENTER);
// Create enhanced toolbar
add(createEnhancedToolbar(), BorderLayout.NORTH);
add(createStatusBar(), BorderLayout.SOUTH);
thumbnailLabels = new ArrayList<>();
}
private JToolBar createEnhancedToolbar() {
JToolBar toolBar = new JToolBar();
JButton openButton = new JButton("Open");
openButton.addActionListener(e -> openPDF());
JButton prevButton = new JButton("◀");
prevButton.addActionListener(e -> showPreviousPage());
JButton nextButton = new JButton("▶");
nextButton.addActionListener(e -> showNextPage());
JButton zoomInButton = new JButton("Zoom In");
zoomInButton.addActionListener(e -> setZoom(zoomFactor * 1.2f));
JButton zoomOutButton = new JButton("Zoom Out");
zoomOutButton.addActionListener(e -> setZoom(zoomFactor / 1.2f));
JButton fitWidthButton = new JButton("Fit Width");
fitWidthButton.addActionListener(e -> fitToWidth());
JButton actualSizeButton = new JButton("100%");
actualSizeButton.addActionListener(e -> setZoom(1.0f));
// Zoom slider
JSlider zoomSlider = new JSlider(25, 400, 100);
zoomSlider.addChangeListener(e -> {
if (!zoomSlider.getValueIsAdjusting()) {
setZoom(zoomSlider.getValue() / 100.0f);
}
});
toolBar.add(openButton);
toolBar.addSeparator();
toolBar.add(prevButton);
toolBar.add(nextButton);
toolBar.addSeparator();
toolBar.add(zoomOutButton);
toolBar.add(zoomInButton);
toolBar.add(fitWidthButton);
toolBar.add(actualSizeButton);
toolBar.addSeparator();
toolBar.add(new JLabel("Zoom:"));
toolBar.add(zoomSlider);
return toolBar;
}
private JPanel createStatusBar() {
JPanel statusPanel = new JPanel(new BorderLayout());
JLabel statusLabel = new JLabel("No document loaded");
statusLabel.setBorder(BorderFactory.createLoweredBevelBorder());
statusPanel.add(statusLabel, BorderLayout.CENTER);
return statusPanel;
}
private void openPDF() {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter(
"PDF Files", "pdf"));
if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
loadPDF(fileChooser.getSelectedFile());
}
}
private void loadPDF(File file) {
try {
// Close previous document
if (document != null) {
document.close();
}
// Load new document
document = PDDocument.load(file);
renderer = new PDFRenderer(document);
currentPage = 0;
zoomFactor = 1.0f;
setTitle("Enhanced PDF Viewer - " + file.getName());
showPage(currentPage);
generateThumbnails();
} catch (IOException ex) {
JOptionPane.showMessageDialog(this,
"Error loading PDF: " + ex.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
private void showPage(int pageNumber) {
if (document == null || pageNumber < 0 || pageNumber >= document.getNumberOfPages()) {
return;
}
try {
// Render page with current zoom factor
BufferedImage image = renderer.renderImage(pageNumber, zoomFactor);
ImageIcon icon = new ImageIcon(image);
imageLabel.setIcon(icon);
currentPage = pageNumber;
updateThumbnailSelection();
updateStatus();
} catch (IOException ex) {
JOptionPane.showMessageDialog(this,
"Error rendering page: " + ex.getMessage(),
"Error", JOptionPane.ERROR_MESSAGE);
}
}
private void generateThumbnails() {
thumbnailsPanel.removeAll();
thumbnailLabels.clear();
if (document == null) return;
try {
int totalPages = document.getNumberOfPages();
for (int i = 0; i < totalPages; i++) {
// Render thumbnail (small version)
BufferedImage thumbImage = renderer.renderImage(i, 0.2f);
ImageIcon thumbIcon = new ImageIcon(thumbImage);
JLabel thumbLabel = new JLabel(thumbIcon);
thumbLabel.setText("Page " + (i + 1));
thumbLabel.setVerticalTextPosition(SwingConstants.BOTTOM);
thumbLabel.setHorizontalTextPosition(SwingConstants.CENTER);
thumbLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
final int pageIndex = i;
thumbLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
showPage(pageIndex);
}
});
thumbnailsPanel.add(thumbLabel);
thumbnailLabels.add(thumbLabel);
}
thumbnailsPanel.revalidate();
thumbnailsPanel.repaint();
updateThumbnailSelection();
} catch (IOException ex) {
ex.printStackTrace();
}
}
private void updateThumbnailSelection() {
for (int i = 0; i < thumbnailLabels.size(); i++) {
JLabel thumbLabel = thumbnailLabels.get(i);
if (i == currentPage) {
thumbLabel.setBorder(BorderFactory.createLineBorder(Color.BLUE, 2));
} else {
thumbLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
}
}
}
private void setZoom(float newZoom) {
if (newZoom < 0.1f) newZoom = 0.1f;
if (newZoom > 5.0f) newZoom = 5.0f;
zoomFactor = newZoom;
if (document != null) {
showPage(currentPage);
}
}
private void fitToWidth() {
if (document == null || scrollPane.getViewport().getWidth() == 0) return;
try {
// Get page dimensions
float pageWidth = document.getPage(currentPage).getMediaBox().getWidth();
float viewportWidth = scrollPane.getViewport().getWidth() - 20; // Padding
zoomFactor = viewportWidth / pageWidth;
showPage(currentPage);
} catch (Exception ex) {
ex.printStackTrace();
}
}
private void showPreviousPage() {
if (currentPage > 0) {
showPage(currentPage - 1);
}
}
private void showNextPage() {
if (document != null && currentPage < document.getNumberOfPages() - 1) {
showPage(currentPage + 1);
}
}
private void updateStatus() {
int totalPages = document != null ? document.getNumberOfPages() : 0;
String status = String.format("Page %d of %d | Zoom: %.0f%%",
currentPage + 1, totalPages, zoomFactor * 100);
// Update status bar
Component[] comps = getContentPane().getComponents();
for (Component comp : comps) {
if (comp instanceof JPanel) {
Component[] subComps = ((JPanel) comp).getComponents();
for (Component subComp : subComps) {
if (subComp instanceof JLabel) {
((JLabel) subComp).setText(status);
return;
}
}
}
}
}
@Override
public void dispose() {
if (document != null) {
try {
document.close();
} catch (IOException e) {
e.printStackTrace();
}
}
super.dispose();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
new EnhancedPDFViewer().setVisible(true);
});
}
}
Text Extraction and Search Feature
Let's add text extraction and search capabilities to our viewer.
Text Extraction and Search Panel:
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import javax.swing.*;
import java.awt.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PDFViewerWithSearch extends EnhancedPDFViewer {
private JTextField searchField;
private JButton searchButton;
private JList<String> searchResultsList;
private DefaultListModel<String> searchResultsModel;
private List<SearchResult> allSearchResults;
public PDFViewerWithSearch() {
super();
addSearchPanel();
}
private void addSearchPanel() {
// Create search panel
JPanel searchPanel = new JPanel(new BorderLayout());
JPanel searchInputPanel = new JPanel(new FlowLayout());
searchField = new JTextField(20);
searchButton = new JButton("Search");
searchInputPanel.add(new JLabel("Search:"));
searchInputPanel.add(searchField);
searchInputPanel.add(searchButton);
searchResultsModel = new DefaultListModel<>();
searchResultsList = new JList<>(searchResultsModel);
searchResultsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
JScrollPane resultsScroll = new JScrollPane(searchResultsList);
resultsScroll.setPreferredSize(new Dimension(250, 150));
searchPanel.add(searchInputPanel, BorderLayout.NORTH);
searchPanel.add(resultsScroll, BorderLayout.CENTER);
// Add search panel to the left of split pane
JSplitPane mainSplitPane = (JSplitPane) getContentPane().getComponent(0);
JSplitPane leftSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
leftSplitPane.setTopComponent(mainSplitPane.getLeftComponent());
leftSplitPane.setBottomComponent(searchPanel);
leftSplitPane.setDividerLocation(400);
mainSplitPane.setLeftComponent(leftSplitPane);
setupSearchListeners();
}
private void setupSearchListeners() {
searchButton.addActionListener(e -> performSearch());
searchField.addActionListener(e -> performSearch());
searchResultsList.addListSelectionListener(e -> {
if (!e.getValueIsAdjusting() && searchResultsList.getSelectedIndex() != -1) {
navigateToSearchResult(searchResultsList.getSelectedIndex());
}
});
}
private void performSearch() {
String searchText = searchField.getText().trim();
if (searchText.isEmpty() || document == null) {
return;
}
searchResultsModel.clear();
allSearchResults = new ArrayList<>();
try {
PDFTextStripper stripper = new PDFTextStripper();
Pattern pattern = Pattern.compile(Pattern.quote(searchText), Pattern.CASE_INSENSITIVE);
for (int page = 0; page < document.getNumberOfPages(); page++) {
stripper.setStartPage(page + 1);
stripper.setEndPage(page + 1);
String pageText = stripper.getText(document);
Matcher matcher = pattern.matcher(pageText);
while (matcher.find()) {
int start = Math.max(0, matcher.start() - 50);
int end = Math.min(pageText.length(), matcher.end() + 50);
String context = pageText.substring(start, end).replaceAll("\\s+", " ").trim();
String resultText = String.format("Page %d: ...%s...", page + 1, context);
searchResultsModel.addElement(resultText);
allSearchResults.add(new SearchResult(page, matcher.start(), context));
}
}
if (searchResultsModel.isEmpty()) {
searchResultsModel.addElement("No results found");
}
} catch (IOException ex) {
JOptionPane.showMessageDialog(this,
"Error searching document: " + ex.getMessage(),
"Search Error", JOptionPane.ERROR_MESSAGE);
}
}
private void navigateToSearchResult(int resultIndex) {
if (allSearchResults != null && resultIndex < allSearchResults.size()) {
SearchResult result = allSearchResults.get(resultIndex);
showPage(result.page);
// Could add highlighting here
}
}
private static class SearchResult {
int page;
int position;
String context;
SearchResult(int page, int position, String context) {
this.page = page;
this.position = position;
this.context = context;
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
new PDFViewerWithSearch().setVisible(true);
});
}
}
Performance Optimization Tips
1. Caching Rendered Pages:
public class PageCache {
private Map<Integer, BufferedImage> cache = new HashMap<>();
private PDFRenderer renderer;
private int maxCacheSize = 10;
public PageCache(PDFRenderer renderer) {
this.renderer = renderer;
}
public BufferedImage getPage(int pageNumber, float zoom) throws IOException {
int key = generateKey(pageNumber, zoom);
if (cache.containsKey(key)) {
return cache.get(key);
}
// Render and cache
BufferedImage image = renderer.renderImage(pageNumber, zoom);
// Manage cache size
if (cache.size() >= maxCacheSize) {
cache.remove(cache.keySet().iterator().next());
}
cache.put(key, image);
return image;
}
private int generateKey(int pageNumber, float zoom) {
return pageNumber * 1000 + (int)(zoom * 100);
}
public void clear() {
cache.clear();
}
}
2. Background Rendering:
private void showPageAsync(int pageNumber) {
SwingWorker<BufferedImage, Void> worker = new SwingWorker<BufferedImage, Void>() {
@Override
protected BufferedImage doInBackground() throws Exception {
return renderer.renderImage(pageNumber, zoomFactor);
}
@Override
protected void done() {
try {
BufferedImage image = get();
ImageIcon icon = new ImageIcon(image);
imageLabel.setIcon(icon);
updateStatus();
} catch (Exception ex) {
ex.printStackTrace();
}
}
};
worker.execute();
}
Best Practices
- Resource Management:
// Always close documents
@Override
public void dispose() {
if (document != null) {
try {
document.close();
} catch (IOException e) {
// Log error
}
}
super.dispose();
}
- Error Handling:
try {
document = PDDocument.load(file);
} catch (IOException ex) {
logger.error("Failed to load PDF", ex);
showErrorDialog("Cannot load PDF file: " + ex.getMessage());
}
- Memory Management:
// For large documents, use memory mapping
PDDocument.load(new File("large.pdf"), MemoryUsageSetting.setupMixed(100 * 1024 * 1024));
Conclusion
Building a custom PDF viewer with Apache PDFBox provides:
- Complete Control: Customize every aspect of the viewing experience
- No External Dependencies: Pure Java solution
- Integration Flexibility: Seamlessly embed in existing applications
- Extensibility: Add custom features like annotation, digital signatures, or specialized rendering
While PDFBox may not match the performance of native PDF renderers for very large documents, it offers an excellent balance of features, flexibility, and ease of integration for most use cases. The examples provided in this article give you a solid foundation for building sophisticated PDF viewing applications tailored to your specific requirements.