基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
// ============================================================
|
||||
// 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
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user