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<String> batchFileListModel;
|
private JList<String> batchFileList;
|
private JTextArea batchLogArea;
|
private List<RectArea> 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<Void, String> worker = new SwingWorker<Void, String>() {
|
@Override
|
protected Void doInBackground() {
|
int processedCount = 0;
|
int successCount = 0;
|
int failCount = 0;
|
|
//初始化excel表头
|
List<List<String>> tableHeader = new ArrayList<>();
|
selectedAreas.forEach(s->tableHeader.add(Collections.singletonList(s.getName())));
|
|
//识别到的数据
|
List<List<String>> 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<String> 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<String> chunks) {
|
for (String message : chunks) {
|
appendLog(message);
|
}
|
}
|
|
@Override
|
protected void done() {
|
appendLog("所有文件处理完成");
|
}
|
};
|
|
worker.execute();
|
}
|
|
/**
|
* 处理单个PDF文件
|
*/
|
private List<String> 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<String> 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<List<RectArea>> typeRef = new TypeReference<List<RectArea>>() {};
|
List<RectArea> 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<RectArea> loadAreasFromConfig(String templatePath) {
|
List<RectArea> areas = new ArrayList<>();
|
|
try {
|
File configDir = new File(CONFIG_DIR);
|
File configFile = new File(configDir, CONFIG_FILE);
|
|
if (!configFile.exists()) {
|
return areas;
|
}
|
|
// 直接读取区域列表,不再检查模板路径
|
TypeReference<List<RectArea>> typeRef = new TypeReference<List<RectArea>>() {};
|
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<RectArea> 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<RectArea> areas = new ArrayList<>();
|
private boolean confirmed = false;
|
private JPanel pdfPreviewPanel;
|
private DefaultListModel<String> areaListModel;
|
private JList<String> 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<RectArea> 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<RectArea> 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());
|
});
|
}
|
|
}
|