package com.xindao.ocr.swingui.swing.jpanel; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.support.ExcelTypeEnum; import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.xindao.ocr.swingui.constant.OcrSwingConstants; import com.xindao.ocr.swingui.service.OcrService; import com.xindao.ocr.swingui.swing.FileProcessorApp; import com.xindao.ocr.swingui.swing.utils.FileNameValidator; import com.xindao.ocr.swingui.swing.utils.GenerateCustomizeComponent; import com.xindao.ocr.swingui.swing.utils.ToFile; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.rendering.PDFRenderer; import javax.swing.*; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.UUID; /** * 多区域处理面板 */ public class MultipleAreaProcessPanel { private Color BACKGROUND_COLOR; private Color PRIMARY_COLOR; private Color TEXT_COLOR; private DefaultListModel batchFileListModel; private JList batchFileList; private JTextArea batchLogArea; private List selectedAreas; // 用于存储选择的区域信息 private OcrService ocrService; private Font DEFAULT_FONT; private FileProcessorApp supper; public MultipleAreaProcessPanel( FileProcessorApp supper, OcrService ocrService, Color BACKGROUND_COLOR, Color PRIMARY_COLOR, Color TEXT_COLOR, Font DEFAULT_FONT) { this.BACKGROUND_COLOR = BACKGROUND_COLOR; this.PRIMARY_COLOR = PRIMARY_COLOR; this.TEXT_COLOR = TEXT_COLOR; this.supper = supper; this.ocrService = ocrService; this.DEFAULT_FONT = DEFAULT_FONT; } public JPanel initPanel() { // 创建功能扩展标签页 JPanel extensionPanel = new JPanel(new BorderLayout(15, 15)); extensionPanel.setBorder(new EmptyBorder(15, 15, 15, 15)); extensionPanel.setBackground(BACKGROUND_COLOR); // 顶部卡片:批量处理操作区域 JPanel topCard2 = GenerateCustomizeComponent.createCardPanel(); topCard2.setLayout(new BoxLayout(topCard2, BoxLayout.Y_AXIS)); topCard2.setBorder(new EmptyBorder(20, 20, 20, 20)); // 添加标题 - 居中显示 JPanel titlePanel2 = new JPanel(new GridBagLayout()); titlePanel2.setOpaque(false); JLabel titleLabel2 = new JLabel("PDF多区域文本识别"); titleLabel2.setFont(new Font(DEFAULT_FONT.getName(), Font.BOLD, 18)); titleLabel2.setForeground(PRIMARY_COLOR); titleLabel2.setBorder(new EmptyBorder(0, 0, 15, 0)); titlePanel2.add(titleLabel2); topCard2.add(titlePanel2); topCard2.add(Box.createVerticalStrut(10)); // 批量处理按钮区域 JPanel batchTopPanel = GenerateCustomizeComponent.createStyledPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JButton selectBatchFilesBtn = GenerateCustomizeComponent.createStyledButton("选择PDF文件", DEFAULT_FONT); JButton selectBatchAreaBtn = GenerateCustomizeComponent.createStyledButton("选择PDF区域", DEFAULT_FONT); JButton removeSelectedBtn = GenerateCustomizeComponent.createStyledButton("移除选中文件", DEFAULT_FONT); JButton clearAllBtn = GenerateCustomizeComponent.createStyledButton("清空列表", DEFAULT_FONT); JButton exportBatchBtn = GenerateCustomizeComponent.createPrimaryButton("处理文件", DEFAULT_FONT); batchTopPanel.add(selectBatchFilesBtn); batchTopPanel.add(selectBatchAreaBtn); batchTopPanel.add(removeSelectedBtn); batchTopPanel.add(clearAllBtn); batchTopPanel.add(exportBatchBtn); // 已选择文件列表区域 JPanel fileListPanel = GenerateCustomizeComponent.createStyledPanel(new BorderLayout()); fileListPanel.setBorder(new EmptyBorder(10, 0, 0, 0)); JLabel fileListTitleLabel = new JLabel("已选择的PDF文件"); fileListTitleLabel.setFont(new Font(DEFAULT_FONT.getName(), Font.BOLD, 14)); fileListTitleLabel.setForeground(TEXT_COLOR); fileListTitleLabel.setBorder(new EmptyBorder(0, 0, 5, 0)); batchFileListModel = new DefaultListModel<>(); batchFileList = new JList<>(batchFileListModel); batchFileList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); batchFileList.setFont(DEFAULT_FONT); JScrollPane fileListScrollPane = new JScrollPane(batchFileList); fileListScrollPane.setPreferredSize(new Dimension(-1, 150)); fileListScrollPane.setBorder(new CompoundBorder( new LineBorder(new Color(220, 220, 220)), new EmptyBorder(5, 5, 5, 5))); fileListPanel.add(fileListTitleLabel, BorderLayout.NORTH); fileListPanel.add(fileListScrollPane, BorderLayout.CENTER); // 添加到顶部卡片 topCard2.add(batchTopPanel); topCard2.add(fileListPanel); // 底部卡片:批量处理日志区域 JPanel bottomCard2 = GenerateCustomizeComponent.createCardPanel(); bottomCard2.setLayout(new BorderLayout()); bottomCard2.setBorder(new EmptyBorder(15, 15, 15, 15)); JLabel logTitleLabel2 = new JLabel("处理日志"); logTitleLabel2.setFont(new Font(DEFAULT_FONT.getName(), Font.BOLD, 14)); logTitleLabel2.setForeground(TEXT_COLOR); logTitleLabel2.setBorder(new EmptyBorder(0, 0, 10, 0)); batchLogArea = new JTextArea(); batchLogArea.setEditable(false); batchLogArea.setLineWrap(true); batchLogArea.setFont(DEFAULT_FONT); batchLogArea.setBackground(new Color(250, 250, 250)); batchLogArea.setBorder(new CompoundBorder( new LineBorder(new Color(220, 220, 220)), new EmptyBorder(5, 5, 5, 5))); JScrollPane logScrollPane = new JScrollPane(batchLogArea); logScrollPane.setBorder(null); logScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); bottomCard2.add(logTitleLabel2, BorderLayout.NORTH); bottomCard2.add(logScrollPane, BorderLayout.CENTER); // 添加顶部卡片和底部卡片到功能扩展标签页 extensionPanel.add(topCard2, BorderLayout.NORTH); extensionPanel.add(bottomCard2, BorderLayout.CENTER); // 添加事件监听器 // 为批量处理按钮添加事件监听器 selectBatchFilesBtn.addActionListener(e -> selectBatchFiles()); selectBatchAreaBtn.addActionListener(e -> loadLastSelectedAreas()); removeSelectedBtn.addActionListener(e -> removeSelectedBatchFiles()); clearAllBtn.addActionListener(e -> clearAllBatchFiles()); exportBatchBtn.addActionListener(e -> batchProcessAndExport()); // 添加一些初始日志信息,验证日志区域是否正常工作 appendLog("PDF多区域文本识别工具已初始化"); appendLog("请选择PDF文件并设置识别区域"); return extensionPanel; } /** * 批量处理文件方法 */ private void batchProcessAndExport(){ if (batchFileListModel.isEmpty()) { JOptionPane.showMessageDialog(supper, "请先选择要处理的文件", "提示", JOptionPane.WARNING_MESSAGE); return; } if (selectedAreas == null || selectedAreas.isEmpty()) { JOptionPane.showMessageDialog(supper, "请先选择PDF区域", "提示", JOptionPane.WARNING_MESSAGE); return; } // 显示文件选择对话框让用户选择输出目录 JFileChooser dirChooser = new JFileChooser(); dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); dirChooser.setDialogTitle("选择输出目录"); int result = dirChooser.showDialog(null, "选择"); if (result != JFileChooser.APPROVE_OPTION) { appendLog("用户取消了输出目录选择"); return; } File outputDirectory = dirChooser.getSelectedFile(); appendLog("输出目录: " + outputDirectory.getAbsolutePath()); appendLog("开始处理文件..."); // 创建一个线程池来并行处理文件 SwingWorker worker = new SwingWorker() { @Override protected Void doInBackground() { int processedCount = 0; int successCount = 0; int failCount = 0; //初始化excel表头 List> tableHeader = new ArrayList<>(); selectedAreas.forEach(s->tableHeader.add(Collections.singletonList(s.getName()))); //识别到的数据 List> tableData = new ArrayList<>(); // 遍历所有选择的文件 for (int i = 0; i < batchFileListModel.size(); i++) { String listItem = batchFileListModel.getElementAt(i); // 从列表项中提取文件路径(假设格式为 "文件名 (路径)") int startIndex = listItem.lastIndexOf('(') + 1; int endIndex = listItem.lastIndexOf(')'); if (startIndex > 0 && endIndex > startIndex) { String filePath = listItem.substring(startIndex, endIndex); File pdfFile = new File(filePath); processedCount++; try { // 处理单个PDF文件 List ocrResults = processSinglePdfFile(pdfFile, outputDirectory); tableData.add(ocrResults); successCount++; appendLog("文件处理成功(" + processedCount + "/" + batchFileListModel.size() + "): " + pdfFile.getName()); } catch (Exception e) { failCount++; appendLog("文件处理失败: " + pdfFile.getName() + " - " + e.getMessage()); e.printStackTrace(); } } } //导出excel文件 try { String outputExcelFileName = "识别结果_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ExcelTypeEnum.XLSX.getValue(); File outputExcelFile = new File(outputDirectory, outputExcelFileName); if (!outputExcelFile.getParentFile().exists()) { outputExcelFile.getParentFile().mkdirs(); } EasyExcel.write(outputExcelFile) .head(tableHeader) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet() .doWrite(tableData); appendLog("文件已导出到: " + outputExcelFile.getAbsolutePath()); } catch (Exception e) { appendLog("文件导出失败: " + e.getMessage()); } // 输出处理统计信息 publish("成功: " + successCount + ",错误: " + failCount); return null; } @Override protected void process(List chunks) { for (String message : chunks) { appendLog(message); } } @Override protected void done() { appendLog("所有文件处理完成"); } }; worker.execute(); } /** * 处理单个PDF文件 */ private List processSinglePdfFile(File pdfFile, File outputDirectory) throws IOException { // 确保输出目录存在 if (!outputDirectory.exists()) { outputDirectory.mkdirs(); } // 创建当前PDF文件的结果目录 String fileNameWithoutExt = pdfFile.getName().substring(0, pdfFile.getName().lastIndexOf('.')); // 加载PDF文档 PDDocument document = null; try { document = PDDocument.load(pdfFile); PDFRenderer renderer = new PDFRenderer(document); List ocrResults = new ArrayList<>(); // 处理每个已选择的区域 for (RectArea area : selectedAreas) { int pageIndex = area.getPageIndex(); if (pageIndex >= document.getNumberOfPages()) { appendLog("警告: 区域\"" + area.getName() + "\"指定的页码不存在,将跳过此区域"); continue; } // 渲染当前页 BufferedImage pageImage = renderer.renderImageWithDPI(pageIndex, 72); // 截取区域图像 BufferedImage areaImage = extractAreaImage(pageImage, area); // 缓存区域图像 //保存图片 File cacheDir = OcrSwingConstants.cacheDir; String outputFilePath =cacheDir.getAbsolutePath() + File.separator + UUID.randomUUID() + ".png"; boolean saved = ToFile.saveImage(areaImage, outputFilePath, "png"); // ImageIO.write(areaImage, "PNG", areaImageFile); if(saved){ // 对区域图像进行OCR识别 String ocrResult = recognizeAreaText(new File(outputFilePath)); ocrResults.add(ocrResult); } } return ocrResults; } finally { if (document != null) { document.close(); } //删除临时目录 ToFile.deleteTempFiles(OcrSwingConstants.cacheDir); } } /** * 从页面图像中截取指定区域的图像 */ private BufferedImage extractAreaImage(BufferedImage pageImage, RectArea area) { // 确保截取区域在图像范围内 int x = Math.max(0, area.getX()); int y = Math.max(0, area.getY()); int width = Math.min(area.getWidth(), pageImage.getWidth() - x); int height = Math.min(area.getHeight(), pageImage.getHeight() - y); // 创建截取的图像 return pageImage.getSubimage(x, y, width, height); } /** * 对区域图像进行OCR文本识别,返回识别到的第一个结果 */ private String recognizeAreaText(File imageFile) throws IOException { // 使用ocrService进行文本识别 String fullText = ocrService.ocr(imageFile.getAbsolutePath()); if(fullText != null && !fullText.isEmpty()){ fullText = FileNameValidator.validateAndCleanFileName(fullText); } return fullText; } // 用于存储选择的PDF模板文件路径 private String selectedTemplatePdfPath = null; // 用于JSON序列化和反序列化的ObjectMapper private static final ObjectMapper objectMapper = new ObjectMapper(); // 存储区域信息的配置文件路径 private static final String CONFIG_DIR = OcrSwingConstants.pdfToolDir.getAbsolutePath(); private static final String CONFIG_FILE = "template_areas.json"; /** * 表示PDF中的一个矩形区域 * 支持JSON序列化和反序列化 */ public static class RectArea { private int pageIndex; // 页码索引 private int x; // GUI中的左上角x像素坐标 private int y; // GUI中的左上角y像素坐标 private int width; // GUI中的宽度像素 private int height; // GUI中的高度像素 private String name; // 区域名称 private float pdfX; // PDF中的左上角x坐标(点) private float pdfY; // PDF中的左上角y坐标(点) private float pdfWidth; // PDF中的宽度(点) private float pdfHeight;// PDF中的高度(点) // 无参构造函数,用于JSON反序列化 public RectArea() { } public RectArea(int pageIndex, int x, int y, int width, int height, String name) { this.pageIndex = pageIndex; this.x = x; this.y = y; this.width = width; this.height = height; this.name = name; // 这些PDF坐标会在转换时设置 this.pdfX = 0; this.pdfY = 0; this.pdfWidth = 0; this.pdfHeight = 0; } // 设置PDF坐标 public void setPdfCoordinates(float pdfX, float pdfY, float pdfWidth, float pdfHeight) { this.pdfX = pdfX; this.pdfY = pdfY; this.pdfWidth = pdfWidth; this.pdfHeight = pdfHeight; } @Override public String toString() { return "页面" + (pageIndex + 1) + " - " + name + " [PDF: (" + String.format("%.2f", pdfX) + "," + String.format("%.2f", pdfY) + "," + String.format("%.2f", pdfWidth) + "," + String.format("%.2f", pdfHeight) + ")]"; } // Getters and setters for JSON serialization public int getPageIndex() { return pageIndex; } public void setPageIndex(int pageIndex) { this.pageIndex = pageIndex; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public String getName() { return name; } public void setName(String name) { this.name = name; } public float getPdfX() { return pdfX; } public void setPdfX(float pdfX) { this.pdfX = pdfX; } public float getPdfY() { return pdfY; } public void setPdfY(float pdfY) { this.pdfY = pdfY; } public float getPdfWidth() { return pdfWidth; } public void setPdfWidth(float pdfWidth) { this.pdfWidth = pdfWidth; } public float getPdfHeight() { return pdfHeight; } public void setPdfHeight(float pdfHeight) { this.pdfHeight = pdfHeight; } } /** * 尝试加载上次保存的区域信息,询问用户是否使用 */ private void selectBatchPdfArea() { selectBatchPdfArea(true); } /** * 尝试加载上次保存的区域信息,询问用户是否使用 */ private void loadLastSelectedAreas() { try { File configDir = new File(CONFIG_DIR); File configFile = new File(configDir, CONFIG_FILE); if (!configFile.exists()) { // 没有配置文件,执行原始的选择流程 selectBatchPdfArea(true); return; } // 直接读取区域列表,不再关联模板文件 TypeReference> typeRef = new TypeReference>() {}; List areas = objectMapper.readValue(configFile, typeRef); if (areas.isEmpty()) { // 配置文件为空,执行原始的选择流程 selectBatchPdfArea(true); return; } // 询问用户是否使用上次的区域配置 int choice = JOptionPane.showConfirmDialog( null, // 使用null作为父组件 "检测到上次保存的区域配置,是否使用?\n\n" + "区域数量: " + areas.size() + " 个", "使用上次的区域配置", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE ); if (choice == JOptionPane.YES_OPTION) { // 用户选择使用上次的配置,但不设置selectedTemplatePdfPath selectedAreas = areas; appendLog("已加载上次保存的区域配置"); // 在日志中显示每个区域的信息 // for (RectArea area : selectedAreas) { // appendLog("区域: " + area.toString()); // } } else { // 用户选择不使用上次的配置,执行原始的选择流程 appendLog("用户选择不使用上次的区域配置"); selectBatchPdfArea(false); } } catch (Exception e) { appendLog("加载上次保存的区域配置失败: " + e.getMessage()); e.printStackTrace(); // 发生异常时,继续使用原始的选择流程 selectBatchPdfArea(true); } } /** * 保存区域信息到配置文件 */ private void saveAreasToConfig() { if (selectedAreas == null || selectedAreas.isEmpty()) { return; } try { // 创建配置目录 File configDir = new File(CONFIG_DIR); if (!configDir.exists()) { configDir.mkdirs(); } // 直接保存区域列表,不再关联模板文件 File configFile = new File(configDir, CONFIG_FILE); objectMapper.writeValue(configFile, selectedAreas); appendLog("区域配置已保存到文件"); } catch (Exception e) { appendLog("保存区域配置失败: " + e.getMessage()); e.printStackTrace(); } } /** * 从配置文件加载区域信息 */ private List loadAreasFromConfig(String templatePath) { List areas = new ArrayList<>(); try { File configDir = new File(CONFIG_DIR); File configFile = new File(configDir, CONFIG_FILE); if (!configFile.exists()) { return areas; } // 直接读取区域列表,不再检查模板路径 TypeReference> typeRef = new TypeReference>() {}; areas = objectMapper.readValue(configFile, typeRef); if (!areas.isEmpty()) { appendLog("已加载 " + areas.size() + " 个保存的区域配置"); } } catch (Exception e) { appendLog("加载区域配置失败: " + e.getMessage()); e.printStackTrace(); } return areas; } /** * 选择PDF区域方法 * @param loadSavedAreas 是否尝试加载已保存的区域配置 */ private void selectBatchPdfArea(boolean loadSavedAreas) { // 创建文件选择器选择模板PDF JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileFilter(new FileNameExtensionFilter("PDF文件 (*.pdf)", "pdf")); int result = fileChooser.showOpenDialog(supper); if (result == JFileChooser.APPROVE_OPTION) { File selectedFile = fileChooser.getSelectedFile(); selectedTemplatePdfPath = selectedFile.getAbsolutePath(); // 是否尝试加载已保存的区域配置 if (loadSavedAreas) { // 首先尝试加载已保存的区域配置 List savedAreas = loadAreasFromConfig(selectedTemplatePdfPath); if (!savedAreas.isEmpty()) { // 如果有保存的区域信息,直接使用 selectedAreas = savedAreas; appendLog("使用已保存的区域配置"); return; } // 如果尝试加载但没有保存的区域信息,继续执行下面的代码打开对话框 } // 无论loadSavedAreas是什么值,只要没有保存的区域信息或用户选择不加载已保存的配置,都打开区域选择对话框 PdfAreaSelectionDialog dialog = new PdfAreaSelectionDialog(selectedTemplatePdfPath); dialog.setModal(true); dialog.setVisible(true); if (dialog.isConfirmed()) { // 获取用户选择的区域 selectedAreas = dialog.getSelectedAreas(); appendLog("已选择 " + selectedAreas.size() + " 个PDF区域"); // 保存用户选择的区域配置 saveAreasToConfig(); } } else { appendLog("用户取消了模板PDF选择"); } } /** * PDF区域选择对话框 */ private class PdfAreaSelectionDialog extends JDialog { private PDDocument document; private int totalPages; private int currentPageIndex = 0; private List areas = new ArrayList<>(); private boolean confirmed = false; private JPanel pdfPreviewPanel; private DefaultListModel areaListModel; private JList areaList; private BufferedImage currentImage; // 当前页面的图像 public PdfAreaSelectionDialog(String pdfPath) { setTitle("选择PDF识别区域"); setSize(900, 700); setLocationRelativeTo(null); try { // 加载PDF文档 document = PDDocument.load(new File(pdfPath)); totalPages = document.getNumberOfPages(); // 尝试从配置文件加载已保存的区域信息 List savedAreas = loadAreasFromConfig(pdfPath); if (!savedAreas.isEmpty()) { areas = savedAreas; appendLog("已加载 " + savedAreas.size() + " 个保存的区域配置到编辑对话框"); } } catch (IOException ex) { appendLog("加载PDF失败: " + ex.getMessage()); JOptionPane.showMessageDialog(this, "加载PDF失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); dispose(); return; } // 创建主面板 JPanel mainPanel = new JPanel(new BorderLayout(10, 10)); mainPanel.setBorder(new EmptyBorder(10, 10, 10, 10)); // 创建PDF预览区域 JPanel previewPanel = new JPanel(new BorderLayout(5, 5)); // 页面控制按钮 JPanel pageControlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5)); JButton prevPageBtn = new JButton("上一页"); JButton nextPageBtn = new JButton("下一页"); JLabel pageLabel = new JLabel("页面: 1 / " + totalPages); pageControlPanel.add(prevPageBtn); pageControlPanel.add(pageLabel); pageControlPanel.add(nextPageBtn); // 创建可滚动的PDF预览面板 JScrollPane scrollablePreview = new JScrollPane(); scrollablePreview.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollablePreview.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); scrollablePreview.setPreferredSize(new Dimension(639, 700)); scrollablePreview.getVerticalScrollBar().setUnitIncrement(16); // 设置滚动速度 scrollablePreview.setBorder(BorderFactory.createEtchedBorder()); // PDF预览面板 pdfPreviewPanel = new JPanel() { private Point startPoint; private Point endPoint; private boolean isDrawing = false; @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 首先绘制PDF图像 if (currentImage != null) { g.drawImage(currentImage, 0, 0, this); } // 绘制选择框(进行中的选择) if (isDrawing && startPoint != null && endPoint != null) { // 绘制选择框 Graphics2D g2d = (Graphics2D) g; g2d.setColor(new Color(255, 0, 0, 100)); // 使用红色填充,更容易看到 int x = Math.min(startPoint.x, endPoint.x); int y = Math.min(startPoint.y, endPoint.y); int width = Math.abs(endPoint.x - startPoint.x); int height = Math.abs(endPoint.y - startPoint.y); g2d.fillRect(x, y, width, height); g2d.setColor(Color.RED); // 红色边框 g2d.setStroke(new BasicStroke(2)); // 加粗边框 g2d.drawRect(x, y, width, height); } // 绘制已保存的区域 for (RectArea area : areas) { if (area.pageIndex == currentPageIndex) { Graphics2D g2d = (Graphics2D) g; g2d.setColor(new Color(0, 255, 0, 80)); // 绿色半透明填充 g2d.fillRect(area.x, area.y, area.width, area.height); g2d.setColor(Color.GREEN); // 绿色边框 g2d.setStroke(new BasicStroke(2)); // 加粗边框 g2d.drawRect(area.x, area.y, area.width, area.height); // 显示区域名称 g2d.setColor(Color.BLUE); g2d.setFont(new Font("宋体", Font.BOLD, 12)); g2d.drawString(area.name, area.x + 5, area.y + 15); } } } { // 初始化鼠标事件 addMouseListener(new java.awt.event.MouseAdapter() { @Override public void mousePressed(java.awt.event.MouseEvent e) { startPoint = e.getPoint(); endPoint = e.getPoint(); isDrawing = true; } @Override public void mouseReleased(java.awt.event.MouseEvent e) { endPoint = e.getPoint(); isDrawing = false; // 计算选择的区域 int x = Math.min(startPoint.x, endPoint.x); int y = Math.min(startPoint.y, endPoint.y); int width = Math.abs(endPoint.x - startPoint.x); int height = Math.abs(endPoint.y - startPoint.y); // 如果选择的区域足够大,添加到列表 if (width > 10 && height > 10) { // 确保对话框在最上层 SwingUtilities.invokeLater(() -> { String areaName = JOptionPane.showInputDialog( PdfAreaSelectionDialog.this, "请输入区域名称:", "区域名称", JOptionPane.PLAIN_MESSAGE); if (areaName != null && !areaName.trim().isEmpty()) { // 创建新区域 RectArea newArea = new RectArea(currentPageIndex, x, y, width, height, areaName.trim()); areas.add(newArea); // 立即转换为PDF坐标 try { PDPage page = document.getPage(currentPageIndex); org.apache.pdfbox.pdmodel.common.PDRectangle mediaBox = page.getMediaBox(); float pageWidth = mediaBox.getWidth(); float pageHeight = mediaBox.getHeight(); // 使用当前已渲染的图像尺寸进行转换 if (currentImage != null) { int imageWidth = currentImage.getWidth(); int imageHeight = currentImage.getHeight(); float xScaleFactor = pageWidth / imageWidth; float yScaleFactor = pageHeight / imageHeight; float pdfX = x * xScaleFactor; float pdfY = pageHeight - (y + height) * yScaleFactor; float pdfWidth = width * xScaleFactor; float pdfHeight = height * yScaleFactor; newArea.setPdfCoordinates(pdfX, pdfY, pdfWidth, pdfHeight); } } catch (Exception ex) { appendLog("添加区域时坐标转换失败: " + ex.getMessage()); } updateAreaList(); pdfPreviewPanel.repaint(); } }); } repaint(); } }); addMouseMotionListener(new java.awt.event.MouseMotionAdapter() { @Override public void mouseDragged(java.awt.event.MouseEvent e) { endPoint = e.getPoint(); repaint(); } }); } }; pdfPreviewPanel.setSize(new Dimension(599,750)); scrollablePreview.setViewportView(pdfPreviewPanel); // 创建带覆盖层的预览面板 JPanel previewWithOverlay = new JPanel(new BorderLayout()); previewWithOverlay.add(scrollablePreview, BorderLayout.CENTER); // 加载第一页 loadPage(currentPageIndex); // 页面控制事件 prevPageBtn.addActionListener(e -> { if (currentPageIndex > 0) { currentPageIndex--; loadPage(currentPageIndex); pageLabel.setText("页面: " + (currentPageIndex + 1) + " / " + totalPages); } }); nextPageBtn.addActionListener(e -> { if (currentPageIndex < totalPages - 1) { currentPageIndex++; loadPage(currentPageIndex); pageLabel.setText("页面: " + (currentPageIndex + 1) + " / " + totalPages); } }); // 添加到预览面板 previewPanel.add(pageControlPanel, BorderLayout.NORTH); previewPanel.add(previewWithOverlay, BorderLayout.CENTER); // 创建区域列表和控制按钮 JPanel rightPanel = new JPanel(new BorderLayout(5, 5)); rightPanel.setPreferredSize(new Dimension(250, -1)); // 区域列表 JPanel areaListPanel = new JPanel(new BorderLayout(5, 5)); JLabel areaListLabel = new JLabel("已选择的区域"); areaListModel = new DefaultListModel<>(); areaList = new JList<>(areaListModel); JScrollPane areaListScrollPane = new JScrollPane(areaList); areaListScrollPane.setPreferredSize(new Dimension(-1, 200)); areaListPanel.add(areaListLabel, BorderLayout.NORTH); areaListPanel.add(areaListScrollPane, BorderLayout.CENTER); // 控制按钮 JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 10)); JButton removeAreaBtn = new JButton("移除选中区域"); JButton clearAreasBtn = new JButton("清空所有区域"); controlPanel.add(removeAreaBtn); controlPanel.add(clearAreasBtn); // 确认和取消按钮 JPanel confirmPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 10, 10)); JButton confirmBtn = new JButton("确认"); JButton cancelBtn = new JButton("取消"); // 渲染时使用的DPI值 final float RENDER_DPI = 72.0f; // PDF默认DPI final float PDF_DPI = 72.0f; // 像素到PDF点的转换因子 float pixelToPointFactor = PDF_DPI / RENDER_DPI; confirmPanel.add(confirmBtn); confirmPanel.add(cancelBtn); // 添加到右侧面板 rightPanel.add(areaListPanel, BorderLayout.NORTH); rightPanel.add(controlPanel, BorderLayout.CENTER); rightPanel.add(confirmPanel, BorderLayout.SOUTH); // 添加到主面板 mainPanel.add(previewPanel, BorderLayout.CENTER); mainPanel.add(rightPanel, BorderLayout.EAST); // 添加事件监听器 removeAreaBtn.addActionListener(e -> { int[] selectedIndices = areaList.getSelectedIndices(); if (selectedIndices != null && selectedIndices.length > 0) { // 从后往前删除,避免索引混乱 for (int i = selectedIndices.length - 1; i >= 0; i--) { areas.remove(selectedIndices[i]); } updateAreaList(); pdfPreviewPanel.repaint(); // 重绘预览面板以更新区域显示 } }); clearAreasBtn.addActionListener(e -> { areas.clear(); updateAreaList(); pdfPreviewPanel.repaint(); // 重绘预览面板以清除所有区域 }); confirmBtn.addActionListener(e -> { // 在确认之前,将所有区域的GUI坐标转换为PDF坐标 try { if (areas.isEmpty()) { appendLog("没有选择任何区域,无需转换坐标"); } else { for (RectArea area : areas) { PDPage page = document.getPage(area.pageIndex); // 获取PDF页面的媒体框(实际尺寸) org.apache.pdfbox.pdmodel.common.PDRectangle mediaBox = page.getMediaBox(); float pageWidth = mediaBox.getWidth(); // PDF页面宽度(点) float pageHeight = mediaBox.getHeight(); // PDF页面高度(点) // appendLog("PDF页面尺寸: 宽度=" + pageWidth + "点, 高度=" + pageHeight + "点"); // 重新渲染当前页面以获取准确的图像尺寸 PDFRenderer renderer = new PDFRenderer(document); BufferedImage pageImage = renderer.renderImageWithDPI(area.pageIndex, RENDER_DPI); int imageWidth = pageImage.getWidth(); // 渲染图像宽度(像素) int imageHeight = pageImage.getHeight(); // 渲染图像高度(像素) // appendLog("渲染图像尺寸: 宽度=" + imageWidth + "像素, 高度=" + imageHeight + "像素"); // 计算水平和垂直方向的转换因子(像素到点) float xScaleFactor = pageWidth / imageWidth; float yScaleFactor = pageHeight / imageHeight; // appendLog("转换因子: x=" + xScaleFactor + ", y=" + yScaleFactor); // 转换x坐标和宽度(水平方向) float pdfX = area.x * xScaleFactor; float pdfWidth = area.width * xScaleFactor; // 转换y坐标(考虑坐标系方向差异) // PDF坐标系原点在左下角,Swing在左上角 float pdfY = area.y * yScaleFactor; float pdfHeight = area.height * yScaleFactor; // 设置PDF坐标 area.setPdfCoordinates(pdfX, pdfY, pdfWidth, pdfHeight); // appendLog("转换后的PDF坐标: (" + pdfX + "," + pdfY + "," + pdfWidth + "," + // pdfHeight + ")"); } // 更新区域列表,确保显示最新的PDF坐标 updateAreaList(); } } catch (Exception ex) { appendLog("坐标转换失败: " + ex.getMessage()); ex.printStackTrace(); // 打印异常堆栈,方便调试 } confirmed = true; dispose(); }); cancelBtn.addActionListener(e -> { confirmed = false; dispose(); }); setContentPane(mainPanel); } // 加载PDF页面 private void loadPage(int pageIndex) { try { PDFRenderer renderer = new PDFRenderer(document); currentImage = renderer.renderImageWithDPI(pageIndex, 72); // 设置面板的首选大小为图像大小 pdfPreviewPanel.setPreferredSize(new Dimension(currentImage.getWidth(), currentImage.getHeight())); pdfPreviewPanel.revalidate(); pdfPreviewPanel.repaint(); } catch (IOException ex) { appendLog("加载PDF页面失败: " + ex.getMessage()); } } // 更新区域列表 private void updateAreaList() { areaListModel.clear(); for (RectArea area : areas) { areaListModel.addElement(area.toString()); } // 确保预览面板正确显示当前页面的所有区域 pdfPreviewPanel.repaint(); } // 获取用户是否确认了选择 public boolean isConfirmed() { return confirmed; } // 重写dispose方法以确保关闭PDF文档 @Override public void dispose() { super.dispose(); if (document != null) { try { document.close(); } catch (Exception e) { // 忽略关闭异常 } } } // 获取用户选择的区域列表 public List getSelectedAreas() { return new ArrayList<>(areas); } } /** * 选择PDF文件方法 */ private void selectBatchFiles() { // 创建文件选择器 JFileChooser fileChooser = new JFileChooser(); // 设置多选模式 fileChooser.setMultiSelectionEnabled(true); // 设置文件过滤器,只显示PDF文件 fileChooser.setFileFilter(new FileNameExtensionFilter("PDF文件 (*.pdf)", "pdf")); // 显示文件选择对话框 int result = fileChooser.showOpenDialog(supper); if (result == JFileChooser.APPROVE_OPTION) { // 获取用户选择的文件 File[] selectedFiles = fileChooser.getSelectedFiles(); if (selectedFiles != null && selectedFiles.length > 0) { int addedCount = 0; // 遍历选择的文件并添加到列表 for (File file : selectedFiles) { String filePath = file.getAbsolutePath(); String fileName = file.getName(); // 检查文件是否已经在列表中 boolean isExist = false; for (int i = 0; i < batchFileListModel.size(); i++) { if (batchFileListModel.getElementAt(i).contains(filePath)) { isExist = true; break; } } if (!isExist) { // 将文件添加到列表中,显示文件名和路径 batchFileListModel.addElement(fileName + " (" + filePath + ")"); addedCount++; } } // 记录日志 appendLog("成功添加 " + addedCount + " 个PDF文件到列表"); } } else { // 用户取消了选择 appendLog("用户取消了文件选择"); } } /** * 移除选中的PDF文件 */ private void removeSelectedBatchFiles() { // 获取选中的索引 int[] selectedIndices = batchFileList.getSelectedIndices(); if (selectedIndices != null && selectedIndices.length > 0) { // 从后往前删除,避免索引混乱 for (int i = selectedIndices.length - 1; i >= 0; i--) { batchFileListModel.remove(selectedIndices[i]); } // 记录日志 appendLog("成功移除 " + selectedIndices.length + " 个选中的PDF文件"); } else { // 没有选中任何文件 appendLog("请先选择要移除的PDF文件"); } } /** * 清空所有PDF文件列表 */ private void clearAllBatchFiles() { if (batchFileListModel.size() > 0) { // 记录要清空的文件数量 int fileCount = batchFileListModel.size(); // 清空列表 batchFileListModel.clear(); // 记录日志 appendLog("成功清空所有 " + fileCount + " 个PDF文件"); } else { // 列表已经为空 appendLog("文件列表已经为空"); } } /** * 向日志区域添加信息 */ public void appendLog(String message) { SwingUtilities.invokeLater(() -> { String timestamp = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()); batchLogArea.append("[" + timestamp + "] " + message + "\n"); // 自动滚动到底部 batchLogArea.setCaretPosition(batchLogArea.getDocument().getLength()); }); } }