基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)

This commit is contained in:
dqb-dev
2026-10-01 22:15:22 +08:00
commit 1e65792b09
499 changed files with 204579 additions and 0 deletions
+221
View File
@@ -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 成套报价软件
{
/// <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);
}
}
}