// ============================================================ // QianfanOcrClient:百度千帆 PaddleOCR-VL-0.9B(版面解析) // · POST https://qianfan.baidubce.com/v2/ocr/paddleocr // Authorization: Bearer {QianfanApiKey}(千帆 ModelBuilder 的 API Key,永久有效) // {"model":"paddleocr-vl-0.9b","file":"<纯base64>","fileType":1} // · 响应 result.layoutParsingResults[0].prunedResult.parsing_res_list[] // 每块 {block_label, block_content, block_bbox[x1,y1,x2,y2](像素)} // → 文本块转 OcrItem;表格块逐行拆(与 GLM 处理一致) // · Key 存 baidu_ocr.json 的 qianfan_api_key(OCR设置窗口写) // ============================================================ using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Net; using System.Text; using System.Threading.Tasks; using System.Web.Script.Serialization; namespace 成套报价软件 { /// 百度千帆 PaddleOCR-VL 客户端(单实例)。 internal sealed class QianfanOcrClient { private readonly JavaScriptSerializer _json = new JavaScriptSerializer(); private string _apiKey; private const string BaseUrl = "https://qianfan.baidubce.com/v2/ocr/paddleocr"; /// 状态提示,接 OcrClient.StatusChanged。 public event Action StatusChanged; public QianfanOcrClient() { try { ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; } catch { } } public bool IsConfigured { get { return !string.IsNullOrWhiteSpace(_apiKey); } } /// Key 由 OcrClient 从 baidu_ocr.json 的 qianfan_api_key 注入(热更新)。 public void SetApiKey(string key) { _apiKey = key == null ? "" : key.Trim(); } /// 识别一块位图 → 文本块列表(带 bbox 像素坐标)。 public Task> RecognizeAsync(Bitmap crop, int timeoutMs) { if (crop == null || crop.Width <= 0 || crop.Height <= 0) return Task.FromResult(new List()); if (!IsConfigured) { Notify("千帆 PaddleOCR-VL 未配置 API Key(OCR 设置窗口里填千帆 API Key)"); var tcs0 = new TaskCompletionSource>(); tcs0.SetException(new Exception( "千帆 PaddleOCR-VL 未配置 API Key。\r\n" + "请到百度智能云千帆平台(ModelBuilder)创建 API Key," + "在 OCR 设置窗口选择引擎\"百度 PaddleOCR-VL\"并填入后保存。")); return tcs0.Task; } return Task.Factory.StartNew(() => { var sw = System.Diagnostics.Stopwatch.StartNew(); var result = RequestAndParse(crop, timeoutMs); Notify("PaddleOCR-VL·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms"); return result; }); } private List RequestAndParse(Bitmap crop, int timeoutMs) { byte[] png; using (var ms = new MemoryStream()) { crop.Save(ms, ImageFormat.Png); png = ms.ToArray(); } // 千帆要求:base64 直传(无 data URI 前缀),fileType=1 图像 string body = _json.Serialize(new Dictionary { { "model", "paddleocr-vl-0.9b" }, { "file", Convert.ToBase64String(png) }, { "fileType", 1 } }); var req = (HttpWebRequest)WebRequest.Create(BaseUrl); req.Method = "POST"; req.ContentType = "application/json"; req.Headers.Add("Authorization", "Bearer " + _apiKey); req.Timeout = timeoutMs; req.ReadWriteTimeout = timeoutMs; req.Proxy = null; var bytes = Encoding.UTF8.GetBytes(body); req.ContentLength = bytes.Length; using (var rs = req.GetRequestStream()) { rs.Write(bytes, 0, bytes.Length); } string respText; try { using (var resp = (HttpWebResponse)req.GetResponse()) using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false))) { respText = sr.ReadToEnd(); } } catch (WebException wex) { string detail = ""; try { if (wex.Response != null) using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false))) detail = sr.ReadToEnd(); } catch { } string hint = wex.Response == null ? ("网络不可达: " + wex.Message) : ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail); throw new Exception("PaddleOCR-VL 请求失败: " + hint); } return ParseResponse(respText, crop.Width, crop.Height); } // ---- 响应解析(行拆分/清理逻辑与 GLM 一致,复用其 shared 方法) ---- private List ParseResponse(string respText, int cropW, int cropH) { var msg = _json.Deserialize>(respText); if (msg == null) throw new Exception("千帆响应解析失败(空响应)"); if (msg.ContainsKey("error")) { var err = msg["error"] as Dictionary; string code = err != null && err.ContainsKey("code") ? Convert.ToString(err["code"]) : "?"; string message = err != null && err.ContainsKey("message") ? Convert.ToString(err["message"]) : respText; throw new Exception("PaddleOCR-VL 错误 [" + code + "]: " + message); } var items = new List(); // result.layoutParsingResults[0].prunedResult.parsing_res_list var result = msg.ContainsKey("result") ? msg["result"] as Dictionary : null; var layouts = result != null && result.ContainsKey("layoutParsingResults") ? result["layoutParsingResults"] as System.Collections.ArrayList : null; if (layouts == null || layouts.Count == 0) throw new Exception("PaddleOCR-VL 响应没有 layoutParsingResults(内容: " + (respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")"); var first = layouts[0] as Dictionary; var pruned = first != null && first.ContainsKey("prunedResult") ? first["prunedResult"] as Dictionary : null; var blocks = pruned != null && pruned.ContainsKey("parsing_res_list") ? pruned["parsing_res_list"] as System.Collections.ArrayList : null; if (blocks == null) return items; foreach (Dictionary b in blocks) { string label = b.ContainsKey("block_label") ? Convert.ToString(b["block_label"]) : ""; string content = b.ContainsKey("block_content") ? Convert.ToString(b["block_content"]) : ""; if (string.IsNullOrWhiteSpace(content)) continue; var bbox = b.ContainsKey("block_bbox") ? b["block_bbox"] as System.Collections.ArrayList : null; if (bbox == null || bbox.Count < 4) continue; // block_bbox 像素坐标 [x1,y1,x2,y2];裁剪到图内 float x1 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[0]))); float y1 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[1]))); float x2 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[2]))); float y2 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[3]))); if (x2 - x1 < 2 || y2 - y1 < 2) continue; if (label == "table") { AddTableItems(items, content, x1, y1, x2, y2); continue; } if (label != "text" && label != "title" && label != "paragraph") continue; // 图片/公式/图表块跳过(title 也是文字,收) content = GlmOcrClient.CleanHtmlShared(content); if (string.IsNullOrWhiteSpace(content)) continue; string[] lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); float lineH = (y2 - y1) / lines.Length; float pad = Math.Min(lineH * 0.15f, 3f); for (int i = 0; i < lines.Length; i++) { string text = GlmOcrClient.CleanMarkdownShared(lines[i]).Trim(); if (text.Length == 0) continue; float ty1 = y1 + lineH * i + pad; float ty2 = y1 + lineH * (i + 1) - pad; if (ty2 - ty1 < 2) { ty1 = y1 + lineH * i; ty2 = ty1 + Math.Max(2, lineH); } items.Add(new OcrItem { Text = text, Box = new[] { new PointF(x1, ty1), new PointF(x2, ty1), new PointF(x2, ty2), new PointF(x1, ty2) }, Score = 1f }); } } return items; } private static readonly System.Text.RegularExpressions.Regex _trRegex = new System.Text.RegularExpressions.Regex("]*>(.*?)", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled); private static readonly System.Text.RegularExpressions.Regex _tdRegex = new System.Text.RegularExpressions.Regex("]*>(.*?)", System.Text.RegularExpressions.RegexOptions.Singleline | System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled); /// 表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。 private static void AddTableItems(List items, string tableHtml, float x1, float y1, float x2, float y2) { var rows = new List(); foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml)) { var cells = new List(); foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value)) { string cell = GlmOcrClient.CleanHtmlShared(td.Groups[1].Value).Trim(); if (cell.Length > 0) cells.Add(cell); } if (cells.Count > 0) rows.Add(string.Join(" ", cells.ToArray())); } if (rows.Count == 0) return; float rowH = (y2 - y1) / rows.Count; float pad = Math.Min(rowH * 0.15f, 3f); for (int i = 0; i < rows.Count; i++) { float ty1 = y1 + rowH * i + pad; float ty2 = y1 + rowH * (i + 1) - pad; if (ty2 - ty1 < 2) { ty1 = y1 + rowH * i; ty2 = ty1 + Math.Max(2, rowH); } items.Add(new OcrItem { Text = rows[i], Box = new[] { new PointF(x1, ty1), new PointF(x2, ty1), new PointF(x2, ty2), new PointF(x1, ty2) }, Score = 1f }); } } private void Notify(string s) { var h = StatusChanged; if (h != null) h(s); } } }