package com.xindao.ocr.swingui.swing.jpanel; import com.alibaba.excel.EasyExcel; import com.alibaba.excel.support.ExcelTypeEnum; import com.xindao.ocr.swingui.constant.OcrSwingConstants; import com.xindao.ocr.swingui.excel.ContractNumberExcelData; 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.commons.lang3.StringUtils; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.util.filetypedetector.FileType; import org.apache.poi.util.IOUtils; import javax.swing.*; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; import javax.swing.filechooser.FileFilter; import java.awt.*; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.prefs.Preferences; /** * 合同编号处理面板 */ public class ContractNumberProcessPanel { private final OcrService ocrService; private final List selectedFiles = new ArrayList<>(); private final Preferences prefs; private final Font DEFAULT_FONT; private JTextArea logArea; private JLabel filesLabel; private JLabel outputDirLabel; private JLabel lastSelectionLabel; // 新增:用于显示上次选择信息的标签 private File outputDirectory; private final Color BACKGROUND_COLOR; private final Color PRIMARY_COLOR; private final Color TEXT_COLOR; private final Color TEXT_LIGHT; // PDF区域选择相关的偏好设置键 private static final String PREF_PDF_PAGE = "lastPdfPage"; private static final String PREF_PDF_X = "lastPdfX"; private static final String PREF_PDF_Y = "lastPdfY"; private static final String PREF_PDF_WIDTH = "lastPdfWidth"; private static final String PREF_PDF_HEIGHT = "lastPdfHeight"; private final FileProcessorApp supper; AtomicInteger fileIndex = new AtomicInteger(1); public ContractNumberProcessPanel( Color BACKGROUND_COLOR, Color PRIMARY_COLOR, Color TEXT_COLOR, Color TEXT_LIGHT, Font font, FileProcessorApp supper, OcrService ocrService){ this.BACKGROUND_COLOR = BACKGROUND_COLOR; this.PRIMARY_COLOR = PRIMARY_COLOR; this.TEXT_COLOR = TEXT_COLOR; this.TEXT_LIGHT = TEXT_LIGHT; this.DEFAULT_FONT = font; this.supper = supper; this.ocrService = ocrService; this.prefs = Preferences.userNodeForPackage(ContractNumberProcessPanel.class); } public JPanel initPanel() { // 初始化面板 // 第一个标签页:文件处理和日志(合并到一个标签页) JPanel mainTab = new JPanel(new BorderLayout(15, 15)); mainTab.setBorder(new EmptyBorder(15, 15, 15, 15)); mainTab.setBackground(BACKGROUND_COLOR); // 顶部卡片:文件选择区域 JPanel topCard = GenerateCustomizeComponent.createCardPanel(); topCard.setLayout(new BoxLayout(topCard, BoxLayout.Y_AXIS)); topCard.setBorder(new EmptyBorder(20, 20, 20, 20)); // 添加标题 - 修改为居中显示 JPanel titlePanel = new JPanel(new GridBagLayout()); titlePanel.setOpaque(false); JLabel titleLabel = new JLabel("合同编号识别"); titleLabel.setFont(new Font(DEFAULT_FONT.getName(), Font.BOLD, 18)); titleLabel.setForeground(PRIMARY_COLOR); titleLabel.setBorder(new EmptyBorder(0, 0, 15, 0)); titlePanel.add(titleLabel); topCard.add(titlePanel); topCard.add(Box.createVerticalStrut(10)); // 文件选择区域 JPanel fileSelectionPanel = GenerateCustomizeComponent.createStyledPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JButton selectFilesBtn = GenerateCustomizeComponent.createStyledButton("选择文件...",DEFAULT_FONT); filesLabel = new JLabel("未选择文件"); filesLabel.setFont(DEFAULT_FONT); filesLabel.setForeground(TEXT_COLOR); fileSelectionPanel.add(selectFilesBtn); fileSelectionPanel.add(filesLabel); // 输出目录选择区域 JPanel outputDirPanel = GenerateCustomizeComponent.createStyledPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JButton selectOutputDirBtn = GenerateCustomizeComponent.createStyledButton("选择输出目录...",DEFAULT_FONT); outputDirLabel = new JLabel("未选择输出目录"); outputDirLabel.setFont(DEFAULT_FONT); outputDirLabel.setForeground(TEXT_COLOR); outputDirPanel.add(selectOutputDirBtn); outputDirPanel.add(outputDirLabel); // PDF区域选择按钮和上次选择信息 - 修改为一行显示 JPanel pdfSelectionPanel = GenerateCustomizeComponent.createStyledPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JButton selectPdfAreaBtn = GenerateCustomizeComponent.createStyledButton("选择PDF区域...",DEFAULT_FONT); pdfSelectionPanel.add(selectPdfAreaBtn); // 上次选择的PDF区域信息 - 使用成员变量引用 lastSelectionLabel = new JLabel("上次选择: 无"); lastSelectionLabel.setFont(DEFAULT_FONT); lastSelectionLabel.setForeground(TEXT_LIGHT); pdfSelectionPanel.add(lastSelectionLabel); // 处理按钮 JPanel processBtnPanel = GenerateCustomizeComponent.createStyledPanel(new FlowLayout(FlowLayout.CENTER, 0, 15)); JButton processBtn = GenerateCustomizeComponent.createPrimaryButton("处理文件",DEFAULT_FONT); processBtnPanel.add(processBtn); // 添加到顶部卡片 topCard.add(fileSelectionPanel); topCard.add(Box.createVerticalStrut(10)); topCard.add(outputDirPanel); topCard.add(Box.createVerticalStrut(10)); topCard.add(pdfSelectionPanel); topCard.add(Box.createVerticalStrut(15)); topCard.add(processBtnPanel); // 底部卡片:日志区域 JPanel bottomCard = GenerateCustomizeComponent.createCardPanel(); bottomCard.setLayout(new BorderLayout()); bottomCard.setBorder(new EmptyBorder(15, 15, 15, 15)); JLabel logTitleLabel = new JLabel("处理日志"); logTitleLabel.setFont(new Font(DEFAULT_FONT.getName(), Font.BOLD, 14)); logTitleLabel.setForeground(TEXT_COLOR); logTitleLabel.setBorder(new EmptyBorder(0, 0, 10, 0)); logArea = new JTextArea(); logArea.setEditable(false); logArea.setLineWrap(true); logArea.setSize(-1,100); logArea.setFont(DEFAULT_FONT); logArea.setBackground(new Color(250, 250, 250)); logArea.setBorder(new CompoundBorder( new LineBorder(new Color(220, 220, 220)), new EmptyBorder(5, 5, 5, 5) )); JScrollPane scrollPane = new JScrollPane(logArea); scrollPane.setBorder(null); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); bottomCard.add(logTitleLabel, BorderLayout.NORTH); bottomCard.add(scrollPane, BorderLayout.CENTER); // 添加顶部卡片和底部卡片到主标签页 mainTab.add(topCard, BorderLayout.NORTH); mainTab.add(bottomCard, BorderLayout.CENTER); // 添加事件监听器 selectFilesBtn.addActionListener(e -> selectFiles()); selectOutputDirBtn.addActionListener(e -> selectOutputDirectory()); processBtn.addActionListener(e -> processFiles()); selectPdfAreaBtn.addActionListener(e -> selectPdfArea()); loadLastPaths(); showLastPdfSelectionInfo(); return mainTab; } private void selectFiles() { JFileChooser fileChooser = new JFileChooser(); setComponentFont(fileChooser, DEFAULT_FONT); String lastFilePath = prefs.get("lastFilepath", ""); if (!lastFilePath.isEmpty()) { File lastFile = new File(lastFilePath); if (lastFile.exists()) { fileChooser.setCurrentDirectory(lastFile.getParentFile()); } } fileChooser.setMultiSelectionEnabled(true); fileChooser.setDialogTitle("选择要处理的文件"); styleFileChooser(fileChooser); int result = fileChooser.showOpenDialog(supper); if (result == JFileChooser.APPROVE_OPTION) { selectedFiles.clear(); File[] files = fileChooser.getSelectedFiles(); selectedFiles.addAll(Arrays.asList(files)); filesLabel.setText("已选择 " + selectedFiles.size() + " 个文件"); log("已选择 " + selectedFiles.size() + " 个文件"); if (files.length > 0) { prefs.put("lastFilepath", files[0].getAbsolutePath()); } } } private void setComponentFont(Component component, Font font) { component.setFont(font); if (component instanceof Container) { for (Component child : ((Container) component).getComponents()) { setComponentFont(child, font); } } } // 美化文件选择器 private void styleFileChooser(JFileChooser chooser) { chooser.setBackground(BACKGROUND_COLOR); chooser.setForeground(TEXT_COLOR); // 设置按钮样式 for (Component comp : chooser.getComponents()) { if (comp instanceof JButton) { JButton btn = (JButton) comp; btn.setFont(DEFAULT_FONT); btn.setBorder(new EmptyBorder(5, 10, 5, 10)); btn.setFocusPainted(false); } setComponentFont(comp, DEFAULT_FONT); } } private void selectOutputDirectory() { JFileChooser dirChooser = new JFileChooser(); setComponentFont(dirChooser, DEFAULT_FONT); String lastDirPath = prefs.get("lastOutputDir", ""); if (!lastDirPath.isEmpty()) { File lastDir = new File(lastDirPath); if (lastDir.exists() && lastDir.isDirectory()) { dirChooser.setCurrentDirectory(lastDir); } } dirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); dirChooser.setDialogTitle("选择输出目录"); styleFileChooser(dirChooser); int result = dirChooser.showOpenDialog(supper); if (result == JFileChooser.APPROVE_OPTION) { outputDirectory = dirChooser.getSelectedFile(); outputDirLabel.setText(outputDirectory.getAbsolutePath()); log("已选择输出目录: " + outputDirectory.getAbsolutePath()); prefs.put("lastOutputDir", outputDirectory.getAbsolutePath()); } } /** * 输出文件 * @param newFileName 新文件名 * @param file 源文件 * @param fileSuffix 文件后缀 * @param targetPath 目标路径 */ private void writeFile(String newFileName,File file,String fileSuffix,File targetPath) throws IOException { String outputFileName = newFileName + fileSuffix; File outputFile = new File(targetPath, outputFileName); if (!outputFile.getParentFile().exists()) { outputFile.getParentFile().mkdirs(); } IOUtils.copy(Files.newInputStream(file.toPath()),outputFile); } /** * 处理合同编号方法 */ private void processFiles() { if (selectedFiles.isEmpty()) { JOptionPane.showMessageDialog(supper, "请先选择要处理的文件", "提示", JOptionPane.WARNING_MESSAGE); return; } if (outputDirectory == null || !outputDirectory.exists()) { JOptionPane.showMessageDialog(supper, "请先选择有效的输出目录", "提示", JOptionPane.WARNING_MESSAGE); return; } log("开始处理文件..."); //识别到的合同编号列表 final List contractNumberList = new CopyOnWriteArrayList<>(); new Thread(() -> { int successCount = 0; int failCount = 0; int processCount = 0; for (File file : selectedFiles) { processCount++; //获取识别到的第一个内容 String text = file.getName().replace(".pdf",""); try { //截取pdf选区图像 String pathStr = capturePdfArea(file, prefs); // ToFile.preprocessImage(pathStr); //读取图像内容 String ocrFullText = FileNameValidator.validateAndCleanFileName(ocrService.ocr(pathStr.replaceFirst("/", ""))); if(StringUtils.isNotBlank(ocrFullText) && !StringUtils.equals(ocrFullText,text)){ text = ocrFullText; String finalText = text; //如果合同编号重复,则在文件名后加一个序号 if(contractNumberList.stream().anyMatch(f -> f.getContractNumber().equals(finalText))){ text+="("+ fileIndex.get() +")"; fileIndex.getAndIncrement(); } //将识别的内容设置为文件名,导出到指定目录 writeFile(text,file, ".pdf",outputDirectory); } successCount++; contractNumberList.add(new ContractNumberExcelData(text)); log("处理成功("+processCount+"/"+selectedFiles.size()+"): " + file.getName()); } catch (Exception e) { failCount++; e.printStackTrace(); log("处理失败: " + file.getName() + " - " + e.getMessage()); //处理失败的文件也输出 try { writeFile(text+"_fail", file, ".pdf",outputDirectory); } catch (IOException ex) { throw new RuntimeException(ex); } }finally { //删除临时目录 ToFile.deleteTempFiles(OcrSwingConstants.cacheDir); } } //导出识别到的合同编号列表 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, ContractNumberExcelData.class).sheet().doWrite(contractNumberList); log("文件已导出到: " + outputExcelFile.getAbsolutePath()); } catch (Exception e) { log("导出合同编号列表失败: " + e.getMessage()); } log("处理完成 - 成功: " + successCount + ", 失败: " + failCount); int finalSuccessCount = successCount; int finalFailCount = failCount; SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(supper, "处理完成\n成功: " + finalSuccessCount + "\n失败: " + finalFailCount, "处理结果", JOptionPane.INFORMATION_MESSAGE) ); }).start(); } // 加载上次的PDF区域选择信息 private Map loadLastPdfSelectionInfo() { Map info = new HashMap<>(); int pageNumber = prefs.getInt(PREF_PDF_PAGE, 0); float x = prefs.getFloat(PREF_PDF_X, 0); float y = prefs.getFloat(PREF_PDF_Y, 0); float width = prefs.getFloat(PREF_PDF_WIDTH, 0); float height = prefs.getFloat(PREF_PDF_HEIGHT, 0); info.put("page", pageNumber); info.put("x", x); info.put("y", y); info.put("width", width); info.put("height", height); return info; } // PDF区域选择功能 private void selectPdfArea() { // 检查是否有上次选择的PDF信息 Map lastSelection = loadLastPdfSelectionInfo(); File pdfFile = null; int defaultPage = 0; boolean hasReSelection = true; // 如果有上次选择的信息,询问用户是否使用 if (!lastSelection.isEmpty()) { int option = JOptionPane.showConfirmDialog(supper, "是否使用上次选择的区域?", "上次选择", JOptionPane.YES_NO_OPTION); if (option == JOptionPane.YES_OPTION) { hasReSelection = false; // 用户选择使用上次的文件 } } // 如果没有上次选择的文件或用户不使用上次的文件,则让用户选择新文件 if (hasReSelection) { JFileChooser fileChooser = new JFileChooser(); setComponentFont(fileChooser, DEFAULT_FONT); styleFileChooser(fileChooser); // 过滤只显示PDF文件 fileChooser.setFileFilter(new FileFilter() { @Override public boolean accept(File f) { return f.isDirectory() || f.getName().toLowerCase().endsWith(".pdf"); } @Override public String getDescription() { return "PDF文件 (*.pdf)"; } }); fileChooser.setDialogTitle("选择PDF文件"); int result = fileChooser.showOpenDialog(supper); if (result != JFileChooser.APPROVE_OPTION) { return; } pdfFile = fileChooser.getSelectedFile(); if (!pdfFile.getName().toLowerCase().endsWith(".pdf")) { JOptionPane.showMessageDialog(supper, "请选择PDF文件", "提示", JOptionPane.WARNING_MESSAGE); return; } // 加载PDF并显示选择面板 try (PDDocument document = PDDocument.load(Files.newInputStream(pdfFile.toPath()))) { int totalPages = document.getNumberOfPages(); // 默认第一页 // 让用户输入页码,默认使用上次的页码 String pageStr = JOptionPane.showInputDialog(supper, "请输入要选择区域的页码(共"+totalPages+"页):", "输入页码", JOptionPane.PLAIN_MESSAGE, null, null, String.valueOf(defaultPage + 1)).toString(); if (pageStr == null || pageStr.trim().isEmpty()) { return; } int pageNumber; try { pageNumber = Integer.parseInt(pageStr.trim()) - 1; // PDFBox页码从0开始 } catch (NumberFormatException e) { JOptionPane.showMessageDialog(supper, "请输入有效的页码", "错误", JOptionPane.ERROR_MESSAGE); return; } if (pageNumber < 0 || pageNumber >= totalPages) { JOptionPane.showMessageDialog(supper, "页码超出范围", "错误", JOptionPane.ERROR_MESSAGE); return; } // 获取PDF页面尺寸 float pdfWidth = document.getPage(pageNumber).getMediaBox().getWidth(); float pdfHeight = document.getPage(pageNumber).getMediaBox().getHeight(); // 获取上次选择的区域(如果存在且是当前文件和页面) Rectangle2D lastArea = null; if (!lastSelection.isEmpty() && pageNumber == (int)lastSelection.get("page")) { float x = (float)lastSelection.get("x"); float y = (float)lastSelection.get("y"); float width = (float)lastSelection.get("width"); float height = (float)lastSelection.get("height"); // 转换为PDF坐标系统的区域(未缩放的) lastArea = new Rectangle2D.Float(x, y, width, height); } // 创建PDF预览和区域选择对话框 JDialog pdfDialog = new JDialog(supper, "选择PDF识别区域 - " + pdfFile.getName(), true); pdfDialog.setSize(639, 850); pdfDialog.setLocationRelativeTo(supper); // 创建PDF预览面板,传入上次选择的区域 PdfPreviewPanel previewPanel = new PdfPreviewPanel(document, pageNumber, lastArea); // 创建可滚动的PDF预览面板 JScrollPane scrollablePreview = new JScrollPane(previewPanel); scrollablePreview.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); scrollablePreview.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); scrollablePreview.setPreferredSize(new Dimension(599, 750)); scrollablePreview.getVerticalScrollBar().setUnitIncrement(16); // 设置滚动速度 scrollablePreview.setBorder(BorderFactory.createEtchedBorder()); pdfDialog.add(scrollablePreview, BorderLayout.CENTER); // 创建底部按钮面板 JPanel buttonPanel = new JPanel(); JButton confirmBtn = GenerateCustomizeComponent.createPrimaryButton("确认选择",DEFAULT_FONT); JButton cancelBtn = GenerateCustomizeComponent.createStyledButton("取消",DEFAULT_FONT); confirmBtn.addActionListener(e -> { Rectangle2D selection = previewPanel.getSelection(); if (selection != null) { // 转换为PDF坐标(考虑缩放和平移) Point translation = previewPanel.getTranslation(); float scale = previewPanel.getScale(); // 计算选择区域在PDF文档中的实际坐标 float pdfX = (float)((selection.getX() - translation.x) / scale); float pdfY = (float)((selection.getY() - translation.y) / scale); float pdfWidthSel = (float)(selection.getWidth() / scale); float pdfHeightSel = (float)(selection.getHeight() / scale); // 保存选择信息 savePdfSelectionInfo(pageNumber, pdfX, pdfY, pdfWidthSel, pdfHeightSel); String coordsInfo = String.format( "新的区域坐标: X: %.2f, Y: %.2f, W: %.2f, H: %.2f", pdfX, pdfY, pdfWidthSel, pdfHeightSel ); log(coordsInfo); // JOptionPane.showMessageDialog(pdfDialog, "确认选择该区域?", "区域坐标", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(pdfDialog, "请先选择区域", "提示", JOptionPane.WARNING_MESSAGE); return; } pdfDialog.dispose(); }); cancelBtn.addActionListener(e -> pdfDialog.dispose()); buttonPanel.add(confirmBtn); buttonPanel.add(cancelBtn); pdfDialog.add(buttonPanel, BorderLayout.SOUTH); pdfDialog.setVisible(true); } catch (Exception e) { log("加载PDF失败: " + e.getMessage()); JOptionPane.showMessageDialog(supper, "加载PDF失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } } // 保存PDF区域选择信息到偏好设置 private void savePdfSelectionInfo(int pageNumber, float x, float y, float width, float height) { // prefs.put(PREF_PDF_PATH, pdfFile.getAbsolutePath()); prefs.putInt(PREF_PDF_PAGE, pageNumber); prefs.putFloat(PREF_PDF_X, x); prefs.putFloat(PREF_PDF_Y, y); prefs.putFloat(PREF_PDF_WIDTH, width); prefs.putFloat(PREF_PDF_HEIGHT, height); // 更新界面显示 showLastPdfSelectionInfo(); } private void loadLastPaths() { String lastDirPath = prefs.get("lastOutputDir", ""); if (!lastDirPath.isEmpty()) { File lastDir = new File(lastDirPath); if (lastDir.exists() && lastDir.isDirectory()) { outputDirectory = lastDir; outputDirLabel.setText(outputDirectory.getAbsolutePath()); log("已加载上次使用的输出目录: " + outputDirectory.getAbsolutePath()); } } } // 显示上次选择的PDF区域信息 - 修复了类型转换问题 private void showLastPdfSelectionInfo() { float x = prefs.getFloat(PREF_PDF_X, 0); float y = prefs.getFloat(PREF_PDF_Y, 0); float width = prefs.getFloat(PREF_PDF_WIDTH, 0); float height = prefs.getFloat(PREF_PDF_HEIGHT, 0); String info = String.format("上次选择区域: - X: %.2f, Y: %.2f, W: %.2f, H: %.2f", x, y, width, height); // 直接使用成员变量更新上次选择信息,避免组件查找 lastSelectionLabel.setText(info); log(info); } private void log(final String message) { SwingUtilities.invokeLater(() -> { String timestamp = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); logArea.append("["+timestamp+"] "+message + "\n"); logArea.setCaretPosition(logArea.getDocument().getLength()); }); } /** * 截取pdf文件指定区域的图像 * @param pdfFile pdf文件 * @param prefs 选区信息 */ private String capturePdfArea(File pdfFile, Preferences prefs) throws IOException { try (PDDocument document = PDDocument.load(Files.newInputStream(pdfFile.toPath()))) { int page = prefs.getInt(PREF_PDF_PAGE, 0); // 转换为用户友好的页码(从1开始) float x = prefs.getFloat(PREF_PDF_X, 0); float y = prefs.getFloat(PREF_PDF_Y, 0); float width = prefs.getFloat(PREF_PDF_WIDTH, 0); float height = prefs.getFloat(PREF_PDF_HEIGHT, 0); if (page < 0 || page > document.getNumberOfPages()) { throw new IllegalArgumentException("页码超出范围: " + page); } PDFRenderer pdfRenderer = new PDFRenderer(document); BufferedImage pageImage = pdfRenderer.renderImage(page); document.close(); BufferedImage croppedImage = cropImage(pageImage, (int) x, (int) y, (int) width, (int) height); //保存图片 File cacheDir = OcrSwingConstants.cacheDir; String outputFilePath =cacheDir.getAbsolutePath() + File.separator + UUID.randomUUID() + ".png"; boolean saved = ToFile.saveImage(croppedImage, outputFilePath, "png"); if(saved){ return outputFilePath; } return ""; } } /** * 裁剪图像指定区域 * @param originalImage 原始图像 * @param x 左上角 x 坐标 * @param y 左上角 y 坐标 * @param width 裁剪宽度 * @param height 裁剪高度 * @return 裁剪后的图像 */ private static BufferedImage cropImage(BufferedImage originalImage, int x, int y, int width, int height) { return originalImage.getSubimage(x, y, width, height); } }