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.