// ============================================================ // UapisOcrClient:UApiPro(uapis.cn) 通用文字识别(带位置) // · POST https://uapis.cn/api/v1/image/ocr (multipart/form-data) // X-API-Key: {key}(uapis.cn 控制台注册生成,免费积分计费 4积分/次,4QPS) // 字段:image_base64(纯base64或DataURI) + need_location="true" // · 响应 words_result[] { words, location{left,top,width,height}, // vertexes_location[], score } → 转 OcrItem(坐标为发送图像素) // · Key 存 baidu_ocr.json 的 uapis_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 pdfbt { /// UApiPro 通用 OCR 客户端(单实例)。 internal sealed class UapisOcrClient { private readonly JavaScriptSerializer _json = new JavaScriptSerializer(); private string _apiKey; private const string BaseUrl = "https://uapis.cn/api/v1/image/ocr"; /// 状态提示,接 OcrClient.StatusChanged。 public event Action StatusChanged; public UapisOcrClient() { try { ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; } catch { } } public bool IsConfigured { get { return !string.IsNullOrWhiteSpace(_apiKey); } } /// Key 由 OcrClient 从 baidu_ocr.json 的 uapis_api_key 注入(热更新)。 public void SetApiKey(string key) { _apiKey = key == null ? "" : key.Trim(); } /// 识别一块位图 → 文本行列表(带位置)。multipart 上传。 public Task> RecognizeAsync(Bitmap crop, int timeoutMs) { if (crop == null || crop.Width <= 0 || crop.Height <= 0) return Task.FromResult(new List()); if (!IsConfigured) { Notify("UApiPro OCR 未配置 API Key(OCR 设置窗口里填 UApiPro Key)"); var tcs0 = new TaskCompletionSource>(); tcs0.SetException(new Exception( "UApiPro(uapis.cn) OCR 未配置 API Key。\r\n" + "请到 uapis.cn/console 注册生成 API Key," + "在 OCR 设置窗口选择引擎\"UApiPro 通用OCR\"并填入后保存。")); return tcs0.Task; } return Task.Factory.StartNew(() => { var sw = System.Diagnostics.Stopwatch.StartNew(); var result = RequestAndParse(crop, timeoutMs); Notify("UApiPro OCR·" + 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(); } string b64 = Convert.ToBase64String(png); // multipart/form-data 手工拼装(net48 无 MultipartFormDataContent) string boundary = "----pdfbt" + Guid.NewGuid().ToString("N"); var body = new MemoryStream(); Action field = (name, value) => { byte[] part = Encoding.UTF8.GetBytes( "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name + "\"\r\n\r\n" + value + "\r\n"); body.Write(part, 0, part.Length); }; field("image_base64", b64); field("need_location", "true"); field("enable_cls", "true"); // 文字方向校正(歪扫图抗性) byte[] tail = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n"); body.Write(tail, 0, tail.Length); var req = (HttpWebRequest)WebRequest.Create(BaseUrl); req.Method = "POST"; req.ContentType = "multipart/form-data; boundary=" + boundary; req.Headers.Add("X-API-Key", _apiKey); req.Timeout = timeoutMs; req.ReadWriteTimeout = timeoutMs; req.Proxy = null; byte[] payload = body.ToArray(); req.ContentLength = payload.Length; using (var rs = req.GetRequestStream()) { rs.Write(payload, 0, payload.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("UApiPro OCR 请求失败: " + hint); } return ParseResponse(respText, crop.Width, crop.Height); } private List ParseResponse(string respText, int cropW, int cropH) { var msg = _json.Deserialize>(respText); if (msg == null) throw new Exception("UApiPro 响应解析失败(空响应)"); // 业务错误:HTTP 200 但 code 非 200/0;或 HTTP 错误体 {code,message} if (msg.ContainsKey("code")) { string code = Convert.ToString(msg["code"]); if (code != "200" && code != "0" && code != "success") { string m = msg.ContainsKey("message") ? Convert.ToString(msg["message"]) : respText; throw new Exception("UApiPro OCR 错误 [" + code + "]: " + m); } } var items = new List(); var arr = msg.ContainsKey("words_result") ? msg["words_result"] as System.Collections.ArrayList : null; if (arr == null) throw new Exception("UApiPro 响应没有 words_result(内容: " + (respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")"); foreach (Dictionary w in arr) { string text = w.ContainsKey("words") ? Convert.ToString(w["words"]) : ""; if (string.IsNullOrWhiteSpace(text)) continue; var loc = w.ContainsKey("location") ? w["location"] as Dictionary : null; if (loc == null) continue; float l = LocF(loc, "left"), t = LocF(loc, "top"); float wd = LocF(loc, "width"), h = LocF(loc, "height"); // 裁剪到图内 l = Math.Max(0, Math.Min(cropW, l)); t = Math.Max(0, Math.Min(cropH, t)); wd = Math.Max(2, Math.Min(cropW - l, wd)); h = Math.Max(2, Math.Min(cropH - t, h)); float score = 1f; if (w.ContainsKey("score")) { float sv; if (float.TryParse(Convert.ToString(w["score"]), out sv)) score = sv; } items.Add(new OcrItem { Text = text, Box = new[] { new PointF(l, t), new PointF(l + wd, t), new PointF(l + wd, t + h), new PointF(l, t + h) }, Score = score }); } return items; } private static float LocF(Dictionary d, string key) { if (d == null || !d.ContainsKey(key)) return 0f; float v; if (float.TryParse(Convert.ToString(d[key]), out v)) return v; return 0f; } private void Notify(string s) { var h = StatusChanged; if (h != null) h(s); } } }