222 lines
8.8 KiB
C#
222 lines
8.8 KiB
C#
// ============================================================
|
|
// 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 成套报价软件
|
|
{
|
|
/// <summary>UApiPro 通用 OCR 客户端(单实例)。</summary>
|
|
internal sealed class UapisOcrClient
|
|
{
|
|
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
|
private string _apiKey;
|
|
private const string BaseUrl = "https://uapis.cn/api/v1/image/ocr";
|
|
|
|
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
|
|
public event Action<string> StatusChanged;
|
|
|
|
public UapisOcrClient()
|
|
{
|
|
try
|
|
{
|
|
ServicePointManager.SecurityProtocol =
|
|
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
public bool IsConfigured
|
|
{
|
|
get { return !string.IsNullOrWhiteSpace(_apiKey); }
|
|
}
|
|
|
|
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 uapis_api_key 注入(热更新)。</summary>
|
|
public void SetApiKey(string key)
|
|
{
|
|
_apiKey = key == null ? "" : key.Trim();
|
|
}
|
|
|
|
/// <summary>识别一块位图 → 文本行列表(带位置)。multipart 上传。</summary>
|
|
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
|
{
|
|
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
|
|
return Task.FromResult(new List<OcrItem>());
|
|
|
|
if (!IsConfigured)
|
|
{
|
|
Notify("UApiPro OCR 未配置 API Key(OCR 设置窗口里填 UApiPro Key)");
|
|
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
|
|
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<OcrItem> 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<string, string> 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<OcrItem> ParseResponse(string respText, int cropW, int cropH)
|
|
{
|
|
var msg = _json.Deserialize<Dictionary<string, object>>(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<OcrItem>();
|
|
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<string, object> 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<string, object> : 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<string, object> 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);
|
|
}
|
|
}
|
|
}
|