京東云OCR識別PDF圖片并批量改名解決方案
一、應用場景
在日常辦公和文檔管理中,經常會遇到大量 PDF 文件需要根據內容進行分類和命名的情況。例如:
- 企業合同管理系統需要根據合同編號、日期等內容自動命名 PDF 文件
- 圖書館數字化項目需要將掃描的圖書章節按照標題命名
- 財務部門需要將發票 PDF 按照發票號碼、金額等信息自動歸類
京東云 OCR 提供了強大的文字識別能力,可以準確識別 PDF 中的文字信息,結合 C# 開發的桌面應用程序,可以實現高效的 PDF 批量改名工作流。
二、界面設計
一個直觀易用的界面設計可以提高工作效率,建議包含以下元素:
- 文件選擇區域:支持拖拽和文件選擇對話框選擇多個 PDF 文件
- OCR 配置區域:選擇 OCR 模板、設置識別語言等
- 預覽區域:顯示原始文件名、識別內容和建議的新文件名
- 處理進度條:顯示當前處理進度和狀態
- 操作按鈕:開始處理、取消、保存設置等
三、詳細代碼步驟解析
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
using Newtonsoft.Json;
using RestSharp;namespace JdCloudOcrPdfRenameTool
{public partial class MainForm : Form{// 京東云OCR配置信息private string accessKeyId = "";private string secretAccessKey = "";private string serviceEndpoint = "https://ocr.jdcloud-api.com/v1/regions/cn-north-1";// 存儲待處理的PDF文件列表private List<string> pdfFiles = new List<string>();// 存儲處理結果private List<RenameItem> renameItems = new List<RenameItem>();public MainForm(){InitializeComponent();InitializeUI();}private void InitializeUI(){// 設置窗體基本屬性this.Text = "JDOCR_PDF圖片識別改名工具";this.Size = new Size(900, 600);this.StartPosition = FormStartPosition.CenterScreen;// 創建控件CreateFileSelectionPanel();CreateOcrConfigPanel();CreatePreviewPanel();CreateActionButtons();// 加載配置LoadSettings();}private Panel fileSelectionPanel;private TextBox txtSelectedFiles;private Button btnSelectFiles;private Button btnClearFiles;private void CreateFileSelectionPanel(){fileSelectionPanel = new Panel{Dock = DockStyle.Top,Height = 100,BorderStyle = BorderStyle.FixedSingle};Label lblFileSelection = new Label{Text = "選擇PDF文件:",Location = new Point(10, 10),AutoSize = true};txtSelectedFiles = new TextBox{Location = new Point(10, 30),Size = new Size(650, 23),ReadOnly = true};btnSelectFiles = new Button{Text = "選擇文件",Location = new Point(670, 30),Size = new Size(100, 23)};btnSelectFiles.Click += BtnSelectFiles_Click;btnClearFiles = new Button{Text = "清除",Location = new Point(780, 30),Size = new Size(100, 23)};btnClearFiles.Click += BtnClearFiles_Click;Label lblDragDrop = new Label{Text = "或者直接將PDF圖片識別文件拖放到此處...",Location = new Point(10, 60),ForeColor = Color.Gray};fileSelectionPanel.Controls.Add(lblFileSelection);fileSelectionPanel.Controls.Add(txtSelectedFiles);fileSelectionPanel.Controls.Add(btnSelectFiles);fileSelectionPanel.Controls.Add(btnClearFiles);fileSelectionPanel.Controls.Add(lblDragDrop);// 設置拖放功能fileSelectionPanel.AllowDrop = true;fileSelectionPanel.DragEnter += FileSelectionPanel_DragEnter;fileSelectionPanel.DragDrop += FileSelectionPanel_DragDrop;this.Controls.Add(fileSelectionPanel);}private Panel ocrConfigPanel;private TextBox txtAccessKey;private TextBox txtSecretKey;private ComboBox cboOcrTemplate;private CheckBox chkOverwrite;private TextBox txtNameFormat;private Button btnSaveSettings;private void CreateOcrConfigPanel(){ocrConfigPanel = new Panel{Dock = DockStyle.Top,Height = 150,BorderStyle = BorderStyle.FixedSingle};Label lblAccessKey = new Label{Text = "Access Key:",Location = new Point(10, 10),Size = new Size(80, 20)};txtAccessKey = new TextBox{Location = new Point(100, 10),Size = new Size(250, 23),Text = accessKeyId};Label lblSecretKey = new Label{Text = "Secret Key:",Location = new Point(10, 40),Size = new Size(80, 20)};txtSecretKey = new TextBox{Location = new Point(100, 40),Size = new Size(250, 23),Text = secretAccessKey,PasswordChar = '*'};Label lblOcrTemplate = new Label{Text = "OCR模板:",Location = new Point(10, 70),Size = new Size(80, 20)};cboOcrTemplate = new ComboBox{Location = new Point(100, 70),Size = new Size(250, 23),DropDownStyle = ComboBoxStyle.DropDownList};cboOcrTemplate.Items.AddRange(new string[] { "通用文字識別", "身份證識別", "營業執照識別", "增值稅發票識別" });cboOcrTemplate.SelectedIndex = 0;Label lblNameFormat = new Label{Text = "命名格式:",Location = new Point(380, 10),Size = new Size(80, 20)};txtNameFormat = new TextBox{Location = new Point(460, 10),Size = new Size(380, 23),Text = "{日期}_{關鍵詞}_{序號}"};Label lblFormatHelp = new Label{Text = "支持變量: {日期}, {時間}, {關鍵詞}, {頁碼}, {序號}, {原文件名}",Location = new Point(380, 40),Size = new Size(500, 20),ForeColor = Color.Gray,Font = new Font(Font, FontStyle.Italic)};chkOverwrite = new CheckBox{Text = "覆蓋已存在文件",Location = new Point(380, 70),Size = new Size(120, 20)};btnSaveSettings = new Button{Text = "保存設置",Location = new Point(740, 100),Size = new Size(100, 23)};btnSaveSettings.Click += BtnSaveSettings_Click;ocrConfigPanel.Controls.Add(lblAccessKey);ocrConfigPanel.Controls.Add(txtAccessKey);ocrConfigPanel.Controls.Add(lblSecretKey);ocrConfigPanel.Controls.Add(txtSecretKey);ocrConfigPanel.Controls.Add(lblOcrTemplate);ocrConfigPanel.Controls.Add(cboOcrTemplate);ocrConfigPanel.Controls.Add(lblNameFormat);ocrConfigPanel.Controls.Add(txtNameFormat);ocrConfigPanel.Controls.Add(lblFormatHelp);ocrConfigPanel.Controls.Add(chkOverwrite);ocrConfigPanel.Controls.Add(btnSaveSettings);this.Controls.Add(ocrConfigPanel);}private Panel previewPanel;private DataGridView dgvPreview;private ProgressBar progressBar;private Label lblProgress;private void CreatePreviewPanel(){previewPanel = new Panel{Dock = DockStyle.Fill,BorderStyle = BorderStyle.FixedSingle};dgvPreview = new DataGridView{Dock = DockStyle.Fill,AutoGenerateColumns = false,SelectionMode = DataGridViewSelectionMode.FullRowSelect,MultiSelect = false};// 添加列dgvPreview.Columns.Add(new DataGridViewTextBoxColumn{HeaderText = "序號",DataPropertyName = "Index",Width = 50});dgvPreview.Columns.Add(new DataGridViewTextBoxColumn{HeaderText = "原始文件名",DataPropertyName = "OriginalFileName",Width = 250});dgvPreview.Columns.Add(new DataGridViewTextBoxColumn{HeaderText = "識別內容",DataPropertyName = "OcrText",Width = 300});dgvPreview.Columns.Add(new DataGridViewTextBoxColumn{HeaderText = "新文件名",DataPropertyName = "NewFileName",Width = 250});dgvPreview.Columns.Add(new DataGridViewTextBoxColumn{HeaderText = "處理狀態",DataPropertyName = "Status",Width = 100});progressBar = new ProgressBar{Dock = DockStyle.Bottom,Height = 20,Visible = false};lblProgress = new Label{Dock = DockStyle.Bottom,Height = 20,TextAlign = ContentAlignment.MiddleLeft,Padding = new Padding(5, 0, 0, 0),Visible = false};previewPanel.Controls.Add(dgvPreview);previewPanel.Controls.Add(progressBar);previewPanel.Controls.Add(lblProgress);this.Controls.Add(previewPanel);}private Button btnProcess;private Button btnRename;private Button btnExport;private void CreateActionButtons(){Panel buttonPanel = new Panel{Dock = DockStyle.Bottom,Height = 40,BorderStyle = BorderStyle.FixedSingle};btnProcess = new Button{Text = "開始識別",Location = new Point(10, 7),Size = new Size(100, 23)};btnProcess.Click += BtnProcess_Click;btnRename = new Button{Text = "執行改名",Location = new Point(120, 7),Size = new Size(100, 23),Enabled = false};btnRename.Click += BtnRename_Click;btnExport = new Button{Text = "導出結果",Location = new Point(230, 7),Size = new Size(100, 23),Enabled = false};btnExport.Click += BtnExport_Click;buttonPanel.Controls.Add(btnProcess);buttonPanel.Controls.Add(btnRename);buttonPanel.Controls.Add(btnExport);this.Controls.Add(buttonPanel);}// 拖放事件處理private void FileSelectionPanel_DragEnter(object sender, DragEventArgs e){if (e.Data.GetDataPresent(DataFormats.FileDrop)){e.Effect = DragDropEffects.Copy;}}private void FileSelectionPanel_DragDrop(object sender, DragEventArgs e){string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);AddPdfFiles(files);}// 按鈕事件處理private void BtnSelectFiles_Click(object sender, EventArgs e){using (OpenFileDialog openFileDialog = new OpenFileDialog()){openFileDialog.Multiselect = true;openFileDialog.Filter = "PDF文件 (*.pdf)|*.pdf|所有文件 (*.*)|*.*";openFileDialog.Title = "選擇PDF文件";if (openFileDialog.ShowDialog() == DialogResult.OK){AddPdfFiles(openFileDialog.FileNames);}}}private void BtnClearFiles_Click(object sender, EventArgs e){pdfFiles.Clear();txtSelectedFiles.Text = "";UpdatePreviewGrid();}private void BtnSaveSettings_Click(object sender, EventArgs e){accessKeyId = txtAccessKey.Text.Trim();secretAccessKey = txtSecretKey.Text.Trim();SaveSettings();MessageBox.Show("設置已保存!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);}private async void BtnProcess_Click(object sender, EventArgs e){if (pdfFiles.Count == 0){MessageBox.Show("請先選擇PDF圖片識別文件!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);return;}if (string.IsNullOrEmpty(accessKeyId) || string.IsNullOrEmpty(secretAccessKey)){MessageBox.Show("請輸入JDXOCR的Access Key和Secret Key!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);return;}// 禁用按鈕防止重復點擊btnProcess.Enabled = false;btnRename.Enabled = false;progressBar.Visible = true;progressBar.Value = 0;progressBar.Maximum = pdfFiles.Count;lblProgress.Visible = true;// 清空之前的結果renameItems.Clear();// 異步處理PDF文件await ProcessPdfFilesAsync();// 更新界面UpdatePreviewGrid();// 恢復按鈕狀態btnProcess.Enabled = true;btnRename.Enabled = renameItems.Any(i => i.Status == "成功");progressBar.Visible = false;lblProgress.Visible = false;}private void BtnRename_Click(object sender, EventArgs e){if (renameItems.Count == 0 || !renameItems.Any(i => i.Status == "成功")){MessageBox.Show("沒有可處理的文件!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);return;}DialogResult result = MessageBox.Show($"確定要將{renameItems.Count(i => i.Status == "成功")}個文件重命名嗎?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);if (result != DialogResult.Yes){return;}int successCount = 0;int failedCount = 0;foreach (var item in renameItems){if (item.Status != "成功"){continue;}try{string directory = Path.GetDirectoryName(item.FilePath);string newFilePath = Path.Combine(directory, item.NewFileName);// 檢查是否需要覆蓋if (File.Exists(newFilePath) && !chkOverwrite.Checked){item.Status = "已存在";failedCount++;continue;}File.Move(item.FilePath, newFilePath);item.Status = "已重命名";successCount++;}catch (Exception ex){item.Status = "失敗";item.ErrorMessage = ex.Message;failedCount++;}}UpdatePreviewGrid();MessageBox.Show($"重命名完成!成功: {successCount}, 失敗: {failedCount}", "結果", MessageBoxButtons.OK, MessageBoxIcon.Information);}private void BtnExport_Click(object sender, EventArgs e){if (renameItems.Count == 0){MessageBox.Show("沒有可導出的結果!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);return;}using (SaveFileDialog saveFileDialog = new SaveFileDialog()){saveFileDialog.Filter = "CSV文件 (*.csv)|*.csv|文本文件 (*.txt)|*.txt";saveFileDialog.Title = "保存結果";saveFileDialog.FileName = "OCR識別結果_" + DateTime.Now.ToString("yyyyMMddHHmmss");if (saveFileDialog.ShowDialog() == DialogResult.OK){try{StringBuilder csvContent = new StringBuilder();csvContent.AppendLine("序號,原始文件名,新文件名,識別內容,處理狀態");foreach (var item in renameItems){string ocrText = item.OcrText.Replace("\n", " ").Replace(",", ",");csvContent.AppendLine($"{item.Index},{item.OriginalFileName},{item.NewFileName},{ocrText},{item.Status}");}File.WriteAllText(saveFileDialog.FileName, csvContent.ToString(), Encoding.UTF8);MessageBox.Show("導出成功!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);}catch (Exception ex){MessageBox.Show($"導出失敗: {ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);}}}}// 輔助方法private void AddPdfFiles(string[] files){int addedCount = 0;foreach (string file in files){if (File.Exists(file) && Path.GetExtension(file).ToLower() == ".pdf"){if (!pdfFiles.Contains(file)){pdfFiles.Add(file);addedCount++;}}}if (addedCount > 0){txtSelectedFiles.Text = $"{pdfFiles.Count}個PDF文件";UpdatePreviewGrid();}}private void UpdatePreviewGrid(){dgvPreview.DataSource = null;dgvPreview.DataSource = renameItems;}private async Task ProcessPdfFilesAsync(){for (int i = 0; i < pdfFiles.Count; i++){string filePath = pdfFiles[i];string fileName = Path.GetFileName(filePath);lblProgress.Text = $"正在處理: {fileName} ({i+1}/{pdfFiles.Count})";progressBar.Value = i + 1;RenameItem item = new RenameItem{Index = i + 1,FilePath = filePath,OriginalFileName = fileName};try{// 從PDF中提取圖片List<byte[]> imageBytesList = ExtractImagesFromPdf(filePath);if (imageBytesList.Count == 0){item.Status = "失敗";item.ErrorMessage = "未從PDF中提取到圖片";renameItems.Add(item);continue;}// 對每張圖片進行OCR識別StringBuilder ocrTextBuilder = new StringBuilder();foreach (var imageBytes in imageBytesList){string ocrText = await PerformOcrAsync(imageBytes);ocrTextBuilder.AppendLine(ocrText);}item.OcrText = ocrTextBuilder.ToString();// 生成新文件名item.NewFileName = GenerateNewFileName(fileName, item.OcrText, i + 1);item.Status = "成功";}catch (Exception ex){item.Status = "失敗";item.ErrorMessage = ex.Message;}renameItems.Add(item);// 更新UIif (dgvPreview.InvokeRequired){dgvPreview.Invoke(new Action(UpdatePreviewGrid));}}}private List<byte[]> ExtractImagesFromPdf(string pdfPath){// 使用iTextSharp庫從PDF中提取圖片識別// 注意:需要添加iTextSharp引用List<byte[]> imageBytesList = new List<byte[]>();try{using (var reader = new iTextSharp.text.pdf.PdfReader(pdfPath)){for (int i = 1; i <= reader.NumberOfPages; i++){var resources = reader.GetPageResources(i);var names = resources.GetResourceNames(iTextSharp.text.pdf.PdfName.XOBJECT);if (names != null){foreach (var name in names){var obj = resources.GetResource(iTextSharp.text.pdf.PdfName.XOBJECT, name);if (obj is iTextSharp.text.pdf.PdfImageObject){var image = (iTextSharp.text.pdf.PdfImageObject)obj;var imageBytes = image.GetImageAsBytes();imageBytesList.Add(imageBytes);}}}}}}catch (Exception ex){throw new Exception($"提取PDF圖片失敗: {ex.Message}");}return imageBytesList;}private async Task<string> PerformOcrAsync(byte[] imageBytes){try{// 創建RestClientvar client = new RestClient(serviceEndpoint);// 根據選擇的OCR模板確定API路徑string apiPath = "/ocr/general"; // 默認為通用文字識別switch (cboOcrTemplate.SelectedIndex){case 0: // 通用文字識別apiPath = "/ocr/general";break;case 1: // 身份證識別apiPath = "/ocr/idcard";break;case 2: // 營業執照識別apiPath = "/ocr/businessLicense";break;case 3: // 增值稅發票識別apiPath = "/ocr/invoice";break;}var request = new RestRequest(apiPath, Method.Post);// 添加認證信息request.AddHeader("Content-Type", "application/json");request.AddHeader("x-jdcloud-access-key", accessKeyId);request.AddHeader("x-jdcloud-signature", GenerateSignature(apiPath, "POST"));// 準備請求體var requestBody = new{image = Convert.ToBase64String(imageBytes)};request.AddJsonBody(requestBody);// 執行請求var response = await client.ExecuteAsync(request);if (!response.IsSuccessful){throw new Exception($"OCR請求失敗: {response.StatusCode} - {response.Content}");}// 解析OCR結果dynamic result = JsonConvert.DeserializeObject(response.Content);// 根據不同的OCR模板解析結果string ocrText = "";if (cboOcrTemplate.SelectedIndex == 0) // 通用文字識別{if (result.code == 0 && result.data != null && result.data.wordsResult != null){foreach (var item in result.data.wordsResult){ocrText += item.words + "\n";}}}else // 其他特定模板識別{if (result.code == 0 && result.data != null){// 不同模板返回的數據結構不同,需要根據實際情況解析ocrText = JsonConvert.SerializeObject(result.data, Formatting.Indented);}}return ocrText.Trim();}catch (Exception ex){throw new Exception($"OCR識別失敗: {ex.Message}");}}private string GenerateSignature(string path, string method){// 注意:這里需要實現京東云的簽名算法// 具體實現可以參考京東云官方文檔:https://docs.jdcloud.com/cn/common-request-signature// 為簡化示例,這里返回一個占位符return "YOUR_GENERATED_SIGNATURE";}private string GenerateNewFileName(string originalFileName, string ocrText, int index){try{string format = txtNameFormat.Text.Trim();if (string.IsNullOrEmpty(format)){format = "{日期}_{關鍵詞}_{序號}";}string extension = Path.GetExtension(originalFileName);string fileNameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);// 提取日期string date = DateTime.Now.ToString("yyyyMMdd");// 提取時間string time = DateTime.Now.ToString("HHmmss");// 提取關鍵詞(從OCR文本中提取前20個字符)string keywords = ocrText.Length > 20 ? ocrText.Substring(0, 20) : ocrText;keywords = Regex.Replace(keywords, @"[^\w\s]", ""); // 移除非法字符keywords = keywords.Replace(" ", "_"); // 替換空格// 構建新文件名string newFileName = format.Replace("{日期}", date).Replace("{時間}", time).Replace("{關鍵詞}", keywords).Replace("{頁碼}", index.ToString()).Replace("{序號}", index.ToString("D3")).Replace("{原文件名}", fileNameWithoutExt);// 確保文件名不包含非法字符foreach (char c in Path.GetInvalidFileNameChars()){newFileName = newFileName.Replace(c, '_');}// 添加文件擴展名return newFileName + extension;}catch (Exception){// 如果生成失敗,使用默認格式return $"OCR_{DateTime.Now:yyyyMMddHHmmss}_{index:D3}{Path.GetExtension(originalFileName)}";}}private void LoadSettings(){try{if (File.Exists("settings.ini")){string[] lines = File.ReadAllLines("settings.ini");foreach (string line in lines){if (string.IsNullOrEmpty(line) || !line.Contains("=")){continue;}string[] parts = line.Split('=');if (parts.Length != 2){continue;}string key = parts[0].Trim();string value = parts[1].Trim();switch (key){case "AccessKey":accessKeyId = value;txtAccessKey.Text = value;break;case "SecretKey":secretAccessKey = value;txtSecretKey.Text = value;break;case "NameFormat":txtNameFormat.Text = value;break;case "Overwrite":chkOverwrite.Checked = value.ToLower() == "true";break;}}}}catch (Exception){// 忽略加載設置時的錯誤}}private void SaveSettings(){try{StringBuilder settings = new StringBuilder();settings.AppendLine($"AccessKey={accessKeyId}");settings.AppendLine($"SecretKey={secretAccessKey}");settings.AppendLine($"NameFormat={txtNameFormat.Text}");settings.AppendLine($"Overwrite={chkOverwrite.Checked}");File.WriteAllText("settings.ini", settings.ToString(), Encoding.UTF8);}catch (Exception){// 忽略保存設置時的錯誤}}}public class RenameItem{public int Index { get; set; }public string FilePath { get; set; }public string OriginalFileName { get; set; }public string OcrText { get; set; }public string NewFileName { get; set; }public string Status { get; set; }public string ErrorMessage { get; set; }}
}
上述代碼實現了一個完整的 PDF 圖片識別改名工具,主要包含以下功能模塊:
界面設計與交互:
- 創建了文件選擇、OCR 配置、預覽和操作按鈕四個主要區域
- 支持拖放和文件選擇對話框選擇 PDF 文件
- 使用 DataGridView 展示處理結果和預覽
PDF 圖片提取:
- 使用 iTextSharp 庫從 PDF 文件中提取圖片
- 支持處理包含多張圖片的 PDF 文件
京東云 OCR 集成:
- 實現了與京東云 OCR API 的通信
- 支持多種 OCR 模板(通用文字、身份證、營業執照、發票等)
- 處理 API 返回結果并提取識別文本
文件名生成規則:
- 支持自定義命名格式,包含多種變量
- 自動處理非法文件名字符
- 提供靈活的命名規則配置
文件重命名功能:
- 支持批量重命名操作
- 提供覆蓋選項和錯誤處理
- 顯示詳細的處理結果和狀態
設置保存與加載:
- 保存和加載用戶配置
- 提供配置持久化功能
四、總結與優化建議
性能優化:
- 對于大量 PDF 文件的處理,可以考慮使用多線程并行處理
- 添加進度保存功能,支持斷點續傳
功能增強:
- 增加 OCR 識別結果編輯功能,允許用戶手動修正識別錯誤
- 添加更多 OCR 模板支持,如表格識別、車牌識別等
- 支持更復雜的命名規則,如正則表達式匹配
用戶體驗優化:
- 添加識別結果預覽和編輯功能
- 增加操作日志記錄,方便追蹤問題
- 支持導出詳細的處理報告
安全與穩定性:
- 改進異常處理機制,增強程序穩定性
- 添加配置加密功能,保護敏感信息
- 增加文件備份選項,防止誤操作
這個工具可以大大提高文檔處理效率,特別是對于需要大量 PDF 文件命名和分類的場景。根據實際需求,你可以進一步定制和擴展這個解決方案。