A2:连接字符串统一收敛(6份+导入报价裸串→数据库连接.串/串无池,调系数/甲方报表/导入报价由裸串升级WAL+超时); A1:OCR/PDF基础设施链接共享(pdfbt链接主项目扒图源11文件,删10个旧副本,pdfbt获得ocr.cls方向纠正等修复); 双项目编译零警告通过,扒图护栏PASS
This commit is contained in:
@@ -1,355 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// GlmOcrClient:智谱 GLM-OCR 云端接口(版面解析 layout_parsing)
|
|
||||||
// · POST {BaseUrl} Authorization: Bearer {ApiKey}
|
|
||||||
// {"model":"glm-ocr","file":"data:image/png;base64,..."}
|
|
||||||
// · 响应 layout_details[页][{label,bbox_2d(归一化),content,width,height}]
|
|
||||||
// → label=text 的块转 OcrItem(bbox 映射回裁剪图像素坐标)
|
|
||||||
// · 手写字识别(模型内置,无需专用参数);价格 ~0.2 元/百万 tokens
|
|
||||||
// · 纯 C# HTTP,无需 Python;引擎切换由 OcrClient 路由
|
|
||||||
// ============================================================
|
|
||||||
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>智谱 GLM-OCR 客户端(单实例)。</summary>
|
|
||||||
internal sealed class GlmOcrClient
|
|
||||||
{
|
|
||||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
|
||||||
private string _apiKey;
|
|
||||||
private string _baseUrl;
|
|
||||||
|
|
||||||
/// <summary>状态提示(未配置Key/请求失败等),接 OcrClient.StatusChanged。</summary>
|
|
||||||
public event Action<string> StatusChanged;
|
|
||||||
|
|
||||||
public GlmOcrClient()
|
|
||||||
{
|
|
||||||
_baseUrl = ReadAppSetting("GlmBaseUrl",
|
|
||||||
"https://open.bigmodel.cn/api/paas/v4/layout_parsing");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// net48 下显式启用 TLS1.2(大模型 API 必须)
|
|
||||||
ServicePointManager.SecurityProtocol =
|
|
||||||
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 glm_api_key 注入(热更新)。</summary>
|
|
||||||
public void SetApiKey(string key)
|
|
||||||
{
|
|
||||||
_apiKey = key == null ? "" : key.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsConfigured
|
|
||||||
{
|
|
||||||
get { return !string.IsNullOrWhiteSpace(_apiKey); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadAppSetting(string key, string def)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var v = System.Configuration.ConfigurationManager.AppSettings[key];
|
|
||||||
return string.IsNullOrWhiteSpace(v) ? def : v;
|
|
||||||
}
|
|
||||||
catch { return def; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</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("GLM OCR 未配置 API Key(OCR 设置窗口里填智谱 API Key)");
|
|
||||||
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
|
|
||||||
tcs0.SetException(new Exception(
|
|
||||||
"智谱 GLM-OCR 未配置 API Key。\r\n请到 open.bigmodel.cn 获取 Key," +
|
|
||||||
"在 OCR 设置窗口选择引擎\"智谱 GLM-OCR\"并填入 Key 后保存。"));
|
|
||||||
return tcs0.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.Factory.StartNew(() =>
|
|
||||||
{
|
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = RequestAndParse(crop, timeoutMs);
|
|
||||||
Notify("智谱GLM-OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
|
|
||||||
{
|
|
||||||
byte[] png;
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
crop.Save(ms, ImageFormat.Png);
|
|
||||||
png = ms.ToArray();
|
|
||||||
}
|
|
||||||
// 服务端期望 data URI 或 URL(SDK 自动包装 data URI,此处显式包装)
|
|
||||||
string fileField = "data:image/png;base64," + Convert.ToBase64String(png);
|
|
||||||
|
|
||||||
string body = _json.Serialize(new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "model", "glm-ocr" },
|
|
||||||
{ "file", fileField }
|
|
||||||
});
|
|
||||||
|
|
||||||
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("GLM OCR 请求失败: " + hint);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseResponse(respText, crop.Width, crop.Height);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>解析 layout_parsing 响应 → OcrItem 列表(text 块)。</summary>
|
|
||||||
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
|
|
||||||
{
|
|
||||||
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
|
|
||||||
if (msg == null) throw new Exception("GLM 响应解析失败(空响应)");
|
|
||||||
if (msg.ContainsKey("error"))
|
|
||||||
{
|
|
||||||
var err = msg["error"] as Dictionary<string, object>;
|
|
||||||
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("GLM OCR 错误 [" + code + "]: " + message);
|
|
||||||
}
|
|
||||||
|
|
||||||
var items = new List<OcrItem>();
|
|
||||||
var pages = msg.ContainsKey("layout_details")
|
|
||||||
? msg["layout_details"] as System.Collections.ArrayList : null;
|
|
||||||
if (pages == null || pages.Count == 0)
|
|
||||||
throw new Exception("GLM 响应没有 layout_details(内容: " +
|
|
||||||
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
|
|
||||||
|
|
||||||
// 单页输入 → 取第一页的块
|
|
||||||
var blocks = pages[0] as System.Collections.ArrayList;
|
|
||||||
if (blocks == null) return items;
|
|
||||||
|
|
||||||
foreach (Dictionary<string, object> b in blocks)
|
|
||||||
{
|
|
||||||
string label = b.ContainsKey("label") ? Convert.ToString(b["label"]) : "";
|
|
||||||
string content = b.ContainsKey("content") ? Convert.ToString(b["content"]) : "";
|
|
||||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
|
||||||
|
|
||||||
var bbox = b.ContainsKey("bbox_2d") ? b["bbox_2d"] as System.Collections.ArrayList : null;
|
|
||||||
if (bbox == null || bbox.Count < 4) continue;
|
|
||||||
|
|
||||||
// ★ 实测返回像素坐标;官方文档描述归一化(0~1)。按值域自动兼容:
|
|
||||||
// 四个值全部 ≤1.5 → 归一化,乘图尺寸;否则直接当像素用。
|
|
||||||
float a0 = Convert.ToSingle(bbox[0]), a1 = Convert.ToSingle(bbox[1]);
|
|
||||||
float a2 = Convert.ToSingle(bbox[2]), a3 = Convert.ToSingle(bbox[3]);
|
|
||||||
bool normalized = a0 <= 1.5f && a1 <= 1.5f && a2 <= 1.5f && a3 <= 1.5f;
|
|
||||||
float x1 = normalized ? a0 * cropW : a0;
|
|
||||||
float y1 = normalized ? a1 * cropH : a1;
|
|
||||||
float x2 = normalized ? a2 * cropW : a2;
|
|
||||||
float y2 = normalized ? a3 * cropH : a3;
|
|
||||||
// 裁剪到图内(超界坐标画在页面外会导致蒙版不可见)
|
|
||||||
x1 = Math.Max(0, Math.Min(cropW, x1));
|
|
||||||
y1 = Math.Max(0, Math.Min(cropH, y1));
|
|
||||||
x2 = Math.Max(0, Math.Min(cropW, x2));
|
|
||||||
y2 = Math.Max(0, Math.Min(cropH, y2));
|
|
||||||
|
|
||||||
if (label == "table")
|
|
||||||
{
|
|
||||||
// ★ 框选多行文字常被判定为表格块(content 为 <table> HTML):
|
|
||||||
// 每个 <tr> 一条文本(单元格空格拼接),行高均分定位。
|
|
||||||
// 必须在 CleanHtml 剥标签之前解析结构。
|
|
||||||
AddTableItems(items, content, x1, y1, x2, y2);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (label != "text") continue; // 图片/公式块跳过
|
|
||||||
content = CleanHtmlShared(content); // 居中块会返回 <div align=..>..</div> 等 HTML
|
|
||||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
|
||||||
|
|
||||||
// 多行文本块拆行(content 按 \n,块内均匀分布);清理 Markdown 标记
|
|
||||||
// ★ 行间上下各收缩 15%(≤3px):蒙版之间留空隙,不连成一片
|
|
||||||
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 = 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("<tr[^>]*>(.*?)</tr>",
|
|
||||||
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("<t[dh][^>]*>(.*?)</t[dh]>",
|
|
||||||
System.Text.RegularExpressions.RegexOptions.Singleline |
|
|
||||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
|
|
||||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
||||||
|
|
||||||
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
|
|
||||||
private static void AddTableItems(List<OcrItem> items, string tableHtml,
|
|
||||||
float x1, float y1, float x2, float y2)
|
|
||||||
{
|
|
||||||
var rows = new List<string>();
|
|
||||||
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
|
|
||||||
{
|
|
||||||
var cells = new List<string>();
|
|
||||||
foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value))
|
|
||||||
{
|
|
||||||
string cell = 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 static readonly System.Text.RegularExpressions.Regex _htmlTag =
|
|
||||||
new System.Text.RegularExpressions.Regex("<[^>]*>",
|
|
||||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 剥离 GLM content 里的 HTML 片段(居中块返回 div 等)与常见实体;
|
|
||||||
/// 标签替换为空格,避免前后文字误粘连;连续空格合并。
|
|
||||||
/// </summary>
|
|
||||||
internal static string CleanHtmlShared(string s)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(s)) return s;
|
|
||||||
if (s.IndexOf('<') >= 0)
|
|
||||||
{
|
|
||||||
s = _htmlTag.Replace(s, " ");
|
|
||||||
s = s.Replace(" ", " ").Replace("&", "&")
|
|
||||||
.Replace("<", "<").Replace(">", ">")
|
|
||||||
.Replace(""", "\"").Replace("'", "'");
|
|
||||||
}
|
|
||||||
while (s.Contains(" ")) s = s.Replace(" ", " ");
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 清理 GLM 返回 content 里的 Markdown 标记(★保守规则,不伤正文):
|
|
||||||
/// · 行首 "##"/"#" 标题井号(仅行首;"2#配电箱" 中间的 # 保留)
|
|
||||||
/// · 成对 **加粗** 星号、行首 "- "/"* " 列表标记
|
|
||||||
/// </summary>
|
|
||||||
internal static string CleanMarkdownShared(string s)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(s)) return s;
|
|
||||||
// 行首标题:# 开头且后跟空格/更多#/行尾 → 剥掉行首所有 #
|
|
||||||
string t = s.TrimStart();
|
|
||||||
if (t.StartsWith("#"))
|
|
||||||
{
|
|
||||||
int i = 0;
|
|
||||||
while (i < t.Length && t[i] == '#') i++;
|
|
||||||
// "#..." 后是空格或行尾才视为标题标记
|
|
||||||
if (i >= t.Length || t[i] == ' ' || t[i] == '\t')
|
|
||||||
t = t.Substring(i).TrimStart();
|
|
||||||
// 否则(如 "#5 线缆")保留原样
|
|
||||||
else t = s;
|
|
||||||
}
|
|
||||||
// 行首列表标记
|
|
||||||
if (t.StartsWith("- ") || t.StartsWith("* "))
|
|
||||||
t = t.Substring(2);
|
|
||||||
// 成对 ** (加粗) 去星号
|
|
||||||
if (t.Contains("**"))
|
|
||||||
t = t.Replace("**", "");
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Notify(string s)
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using 成套报价软件; // ★ A1:OCR/PDF 基础设施已链接共享主项目源码(类型在 成套报价软件 命名空间)
|
||||||
namespace pdfbt
|
namespace pdfbt
|
||||||
{
|
{
|
||||||
public partial class MainForm : Form
|
public partial class MainForm : Form
|
||||||
@@ -32,7 +32,7 @@ namespace pdfbt
|
|||||||
.Deserialize<System.Collections.Generic.Dictionary<string, string>>(
|
.Deserialize<System.Collections.Generic.Dictionary<string, string>>(
|
||||||
File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json")));
|
File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json")));
|
||||||
string eng = jsv != null && jsv.ContainsKey("engine") ? jsv["engine"] : "baidu";
|
string eng = jsv != null && jsv.ContainsKey("engine") ? jsv["engine"] : "baidu";
|
||||||
if (eng == "umi" && UmiOcrHost.IsInstalled) UmiOcrHost.EnsureRunning();
|
if (eng == "umi" && UmiOcrHost.IsInstalled) UmiOcrLauncher.EnsureRunning(); // ★ A1:启停已拆到 UmiOcrLauncher(主项目共享版)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|||||||
@@ -1,417 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// OcrClient:OCR 引擎路由(baidu_ocr.json 的 engine 字段选择)
|
|
||||||
// · engine=baidu(默认):百度 OCR 高精度含位置版(HTTP 直连)
|
|
||||||
// · engine=glm:智谱 GLM-OCR 版面解析(手写字强,GlmOcrClient)
|
|
||||||
// · Key 都在 exe 旁 baidu_ocr.json(OCR设置窗口写):
|
|
||||||
// api_key/secret_key=百度;glm_api_key=智谱
|
|
||||||
// 百度未保存过/Key 为空 → 内置默认 Key 直接可用
|
|
||||||
// · 百度 access_token 缓存 exe 旁 baidu_token.json(29 天,提前1天过期)
|
|
||||||
// · 未配 Key / 网络失败直接抛异常(界面提示),无本地回退
|
|
||||||
// ============================================================
|
|
||||||
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
|
|
||||||
{
|
|
||||||
public class OcrItem
|
|
||||||
{
|
|
||||||
public string Text;
|
|
||||||
public PointF[] Box; // 裁剪图像素坐标,4 点(左上起顺时针)
|
|
||||||
public float Score;
|
|
||||||
|
|
||||||
public RectangleF Bounds
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (Box == null || Box.Length == 0) return RectangleF.Empty;
|
|
||||||
float l = Box[0].X, t = Box[0].Y, r = Box[0].X, b = Box[0].Y;
|
|
||||||
foreach (var p in Box)
|
|
||||||
{
|
|
||||||
if (p.X < l) l = p.X; if (p.X > r) r = p.X;
|
|
||||||
if (p.Y < t) t = p.Y; if (p.Y > b) b = p.Y;
|
|
||||||
}
|
|
||||||
return RectangleF.FromLTRB(l, t, r, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class OcrClient : IDisposable
|
|
||||||
{
|
|
||||||
public event Action<string> StatusChanged;
|
|
||||||
|
|
||||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
|
||||||
private string _ak = "", _sk = "";
|
|
||||||
private string _token;
|
|
||||||
private DateTime _tokenExpire = DateTime.MinValue;
|
|
||||||
|
|
||||||
// ★ 引擎路由:baidu(默认)/glm(智谱 GLM-OCR)/qianfan(百度 PaddleOCR-VL)/uapis(UApiPro)/umi(Umi-OCR 本地)
|
|
||||||
private string _engine = "baidu";
|
|
||||||
private readonly GlmOcrClient _glm = new GlmOcrClient();
|
|
||||||
private readonly QianfanOcrClient _qianfan = new QianfanOcrClient();
|
|
||||||
private readonly UapisOcrClient _uapis = new UapisOcrClient();
|
|
||||||
private readonly UmiOcrClient _umi = new UmiOcrClient();
|
|
||||||
|
|
||||||
/// <summary>当前 OCR 引擎(baidu/glm/qianfan/uapis/umi)。</summary>
|
|
||||||
public string Engine
|
|
||||||
{
|
|
||||||
get { return _engine; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内置默认 Key(Base64 混淆,exe 内非明文):未保存过配置/Key 为空时直接使用
|
|
||||||
private static readonly string _defAk = Encoding.UTF8.GetString(Convert.FromBase64String("R0hjbjVxRmJLcG5TY3NyZHlIV1M4dzBB"));
|
|
||||||
private static readonly string _defSk = Encoding.UTF8.GetString(Convert.FromBase64String("VkMxVVU2RXlxN28wcGRLNElCZ1A4ZnNhWjIyQ3RlMWM="));
|
|
||||||
|
|
||||||
public OcrClient()
|
|
||||||
{
|
|
||||||
_glm.StatusChanged += msg =>
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(msg);
|
|
||||||
};
|
|
||||||
_qianfan.StatusChanged += msg =>
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(msg);
|
|
||||||
};
|
|
||||||
_uapis.StatusChanged += msg =>
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(msg);
|
|
||||||
};
|
|
||||||
_umi.StatusChanged += msg =>
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(msg);
|
|
||||||
};
|
|
||||||
LoadConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>读 baidu_ocr.json(OCR设置窗口保存后调用可热更新 Key/引擎)。</summary>
|
|
||||||
public void LoadConfig()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string p = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json");
|
|
||||||
if (File.Exists(p))
|
|
||||||
{
|
|
||||||
var jsv = _json.Deserialize<Dictionary<string, string>>(File.ReadAllText(p));
|
|
||||||
if (jsv != null)
|
|
||||||
{
|
|
||||||
_ak = jsv.ContainsKey("api_key") ? (jsv["api_key"] ?? "") : "";
|
|
||||||
_sk = jsv.ContainsKey("secret_key") ? (jsv["secret_key"] ?? "") : "";
|
|
||||||
_engine = jsv.ContainsKey("engine") ? (jsv["engine"] ?? "baidu") : "baidu";
|
|
||||||
_glm.SetApiKey(jsv.ContainsKey("glm_api_key") ? (jsv["glm_api_key"] ?? "") : "");
|
|
||||||
_qianfan.SetApiKey(jsv.ContainsKey("qianfan_api_key") ? (jsv["qianfan_api_key"] ?? "") : "");
|
|
||||||
_uapis.SetApiKey(jsv.ContainsKey("uapis_api_key") ? (jsv["uapis_api_key"] ?? "") : "");
|
|
||||||
_umi.SetBaseUrl(jsv.ContainsKey("umi_url") ? (jsv["umi_url"] ?? "") : "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
// 缺省回退内置默认 Key:装完即用,无需打开设置点保存;用户保存过则文件值优先
|
|
||||||
if (string.IsNullOrEmpty(_ak)) _ak = _defAk;
|
|
||||||
if (string.IsNullOrEmpty(_sk)) _sk = _defSk;
|
|
||||||
if (_engine != "baidu" && _engine != "glm" && _engine != "qianfan" && _engine != "uapis" && _engine != "umi")
|
|
||||||
_engine = "baidu";
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasKey
|
|
||||||
{
|
|
||||||
get { return !string.IsNullOrEmpty(_ak) && !string.IsNullOrEmpty(_sk); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Notify(string msg)
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>获取 access_token(内存→文件缓存→在线换取)。失败抛异常。</summary>
|
|
||||||
private string GetToken()
|
|
||||||
{
|
|
||||||
if (_token != null && DateTime.Now < _tokenExpire) return _token;
|
|
||||||
|
|
||||||
string cachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_token.json");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(cachePath))
|
|
||||||
{
|
|
||||||
var c = _json.Deserialize<Dictionary<string, object>>(File.ReadAllText(cachePath));
|
|
||||||
if (c != null && c.ContainsKey("token") && c.ContainsKey("expire"))
|
|
||||||
{
|
|
||||||
double exp;
|
|
||||||
double.TryParse(Convert.ToString(c["expire"]), out exp);
|
|
||||||
string tok = Convert.ToString(c["token"]);
|
|
||||||
if (tok.Length > 0 && DateTime.Now < new DateTime(1970, 1, 1).AddSeconds(exp))
|
|
||||||
{
|
|
||||||
_token = tok;
|
|
||||||
_tokenExpire = new DateTime(1970, 1, 1).AddSeconds(exp);
|
|
||||||
return _token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
if (!HasKey)
|
|
||||||
throw new Exception("未配置百度 OCR Key(工具栏 OCR设置 填写 API Key / Secret Key)");
|
|
||||||
|
|
||||||
string url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
|
|
||||||
+ "&client_id=" + Uri.EscapeDataString(_ak)
|
|
||||||
+ "&client_secret=" + Uri.EscapeDataString(_sk);
|
|
||||||
using (var wc = new WebClient())
|
|
||||||
{
|
|
||||||
wc.Encoding = Encoding.UTF8;
|
|
||||||
wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
||||||
string body = wc.UploadString(url, "POST", "");
|
|
||||||
var resp = _json.Deserialize<Dictionary<string, object>>(body);
|
|
||||||
if (resp == null || !resp.ContainsKey("access_token"))
|
|
||||||
throw new Exception("百度 token 获取失败: " +
|
|
||||||
(resp != null && resp.ContainsKey("error_description")
|
|
||||||
? Convert.ToString(resp["error_description"])
|
|
||||||
: body.Substring(0, Math.Min(120, body.Length))));
|
|
||||||
_token = Convert.ToString(resp["access_token"]);
|
|
||||||
double expires;
|
|
||||||
double.TryParse(Convert.ToString(resp.ContainsKey("expires_in") ? resp["expires_in"] : "2592000"), out expires);
|
|
||||||
_tokenExpire = DateTime.Now.AddSeconds(Math.Max(3600, expires - 86400));
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var cache = new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "token", _token },
|
|
||||||
{ "expire", (_tokenExpire - new DateTime(1970, 1, 1)).TotalSeconds }
|
|
||||||
};
|
|
||||||
File.WriteAllText(cachePath, _json.Serialize(cache));
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return _token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>识别位图(300DPI 裁剪)。失败抛异常;成功通知引擎来源。
|
|
||||||
/// 竖排文字(实为旋转 90° 的横排)直接识别必乱码:细高框或平均置信度偏低时,
|
|
||||||
/// 旋转 ±90° 重识别并按平均置信度选优,坐标逆向还原回原图。</summary>
|
|
||||||
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
|
||||||
{
|
|
||||||
// ★ 引擎路由:智谱 GLM-OCR / 千帆 PaddleOCR-VL(版面解析,不走旋转重试)
|
|
||||||
if (_engine == "glm")
|
|
||||||
return _glm.RecognizeAsync(crop, timeoutMs);
|
|
||||||
if (_engine == "qianfan")
|
|
||||||
return _qianfan.RecognizeAsync(crop, timeoutMs);
|
|
||||||
if (_engine == "uapis")
|
|
||||||
return _uapis.RecognizeAsync(crop, timeoutMs);
|
|
||||||
if (_engine == "umi")
|
|
||||||
return _umi.RecognizeAsync(crop, timeoutMs);
|
|
||||||
|
|
||||||
var tcs = new TaskCompletionSource<List<OcrItem>>();
|
|
||||||
Task.Factory.StartNew(delegate
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var items = RecognizeOnce(crop, timeoutMs);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (crop.Height >= crop.Width * 1.15f || AvgScore(items) < 0.88f)
|
|
||||||
{
|
|
||||||
float best = AvgScore(items);
|
|
||||||
foreach (bool cw in new bool[] { true, false })
|
|
||||||
{
|
|
||||||
using (var turned = new Bitmap(crop))
|
|
||||||
{
|
|
||||||
turned.RotateFlip(cw ? RotateFlipType.Rotate90FlipNone : RotateFlipType.Rotate270FlipNone);
|
|
||||||
var rr = RecognizeOnce(turned, timeoutMs);
|
|
||||||
UnrotateItems(rr, cw, crop.Width, crop.Height);
|
|
||||||
float s = AvgScore(rr);
|
|
||||||
if (s > best + 0.01f) { best = s; items = rr; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* 旋转重试失败:保留原方向结果 */ }
|
|
||||||
Notify("百度OCR·高精度含位置版");
|
|
||||||
tcs.TrySetResult(items);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
tcs.TrySetException(ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return tcs.Task;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>单方向识别一次(编码→上传→解析);Box 为入参图像的像素坐标。</summary>
|
|
||||||
private List<OcrItem> RecognizeOnce(Bitmap img, int timeoutMs)
|
|
||||||
{
|
|
||||||
// 编码为百度限额内(高精度版 base64 后 ≤4MB)的 JPEG;返回缩放比用于还原识别坐标
|
|
||||||
float sendScale;
|
|
||||||
string b64 = EncodeFit(img, out sendScale);
|
|
||||||
string tok = GetToken();
|
|
||||||
string url = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate?access_token="
|
|
||||||
+ Uri.EscapeDataString(tok);
|
|
||||||
// Base64 的表单转义不能用 Uri.EscapeDataString:.NET Framework 对超长字符串
|
|
||||||
// (约 32K 字符)抛"无效的 URI: URI 字符串太长",大图 base64 必中
|
|
||||||
string form = "image=" + EscapeBase64(b64)
|
|
||||||
+ "&language_type=CHN_ENG&probability=true";
|
|
||||||
using (var wc = new WebClient())
|
|
||||||
{
|
|
||||||
wc.Encoding = Encoding.UTF8;
|
|
||||||
wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
||||||
string body = wc.UploadString(url, "POST", form);
|
|
||||||
var resp = _json.Deserialize<Dictionary<string, object>>(body);
|
|
||||||
if (resp == null)
|
|
||||||
throw new Exception("百度OCR返回解析失败");
|
|
||||||
if (resp.ContainsKey("error_code") && Convert.ToInt64(resp["error_code"]) != 0)
|
|
||||||
throw new Exception("百度OCR错误 " + Convert.ToString(resp["error_code"]) + ": "
|
|
||||||
+ Convert.ToString(resp.ContainsKey("error_msg") ? resp["error_msg"] : ""));
|
|
||||||
var items = new List<OcrItem>();
|
|
||||||
var arr = resp.ContainsKey("words_result") ? resp["words_result"] as System.Collections.ArrayList : null;
|
|
||||||
if (arr != null)
|
|
||||||
{
|
|
||||||
foreach (Dictionary<string, object> w in arr)
|
|
||||||
{
|
|
||||||
var loc = (w != null && w.ContainsKey("location")) ? w["location"] as Dictionary<string, object> : null;
|
|
||||||
if (loc == null) continue;
|
|
||||||
float l = LocF(loc, "left"), t = LocF(loc, "top");
|
|
||||||
float w2 = LocF(loc, "width"), h = LocF(loc, "height");
|
|
||||||
float score = 0.95f;
|
|
||||||
var prob = w.ContainsKey("probability") ? w["probability"] as Dictionary<string, object> : null;
|
|
||||||
if (prob != null && prob.ContainsKey("average"))
|
|
||||||
{
|
|
||||||
float sv;
|
|
||||||
if (float.TryParse(Convert.ToString(prob["average"]), out sv)) score = sv;
|
|
||||||
}
|
|
||||||
items.Add(new OcrItem
|
|
||||||
{
|
|
||||||
Text = Convert.ToString(w.ContainsKey("words") ? w["words"] : ""),
|
|
||||||
// location 是发送图的像素坐标,除以缩放比还原到原始裁剪图坐标
|
|
||||||
Box = new PointF[]
|
|
||||||
{
|
|
||||||
new PointF(l / sendScale, t / sendScale),
|
|
||||||
new PointF((l + w2) / sendScale, t / sendScale),
|
|
||||||
new PointF((l + w2) / sendScale, (t + h) / sendScale),
|
|
||||||
new PointF(l / sendScale, (t + h) / sendScale)
|
|
||||||
},
|
|
||||||
Score = score
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>平均置信度(无结果时 -1,便于选优时落败)。</summary>
|
|
||||||
private static float AvgScore(List<OcrItem> items)
|
|
||||||
{
|
|
||||||
if (items == null || items.Count == 0) return -1f;
|
|
||||||
float sum = 0;
|
|
||||||
foreach (var it in items) sum += it.Score;
|
|
||||||
return sum / items.Count;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>把顺/逆时针旋转 90° 后识别出的 Box 坐标逆向还原到原图坐标系
|
|
||||||
/// (还原后重排为 左上/右上/右下/左下,Bounds 语义不变)。</summary>
|
|
||||||
private static void UnrotateItems(List<OcrItem> items, bool clockwise, int w, int h)
|
|
||||||
{
|
|
||||||
foreach (var it in items)
|
|
||||||
{
|
|
||||||
if (it.Box == null) continue;
|
|
||||||
float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;
|
|
||||||
foreach (var p in it.Box)
|
|
||||||
{
|
|
||||||
float x, y;
|
|
||||||
if (clockwise) { x = p.Y; y = h - 1f - p.X; }
|
|
||||||
else { x = w - 1f - p.Y; y = p.X; }
|
|
||||||
if (x < l) l = x; if (x > r) r = x;
|
|
||||||
if (y < t) t = y; if (y > b) b = y;
|
|
||||||
}
|
|
||||||
it.Box = new PointF[]
|
|
||||||
{
|
|
||||||
new PointF(l, t), new PointF(r, t),
|
|
||||||
new PointF(r, b), new PointF(l, b)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static float LocF(Dictionary<string, object> loc, string key)
|
|
||||||
{
|
|
||||||
float v;
|
|
||||||
return (loc != null && loc.ContainsKey(key) &&
|
|
||||||
float.TryParse(Convert.ToString(loc[key]), out v)) ? v : 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>编码为百度限额内的 JPEG(高精度版要求 base64 后 ≤4MB):
|
|
||||||
/// 质量 90/75/60 递降试编码,均超限则按 0.8 倍降采样重试(最多 8 轮,必然收敛)。
|
|
||||||
/// scale=发送图/原图(≤1),调用方用它把返回坐标还原到原图。</summary>
|
|
||||||
private static string EncodeFit(Bitmap img, out float scale)
|
|
||||||
{
|
|
||||||
scale = 1f;
|
|
||||||
string last = null;
|
|
||||||
for (int round = 0; round < 8; round++)
|
|
||||||
{
|
|
||||||
int w = Math.Max(1, (int)Math.Round(img.Width * scale));
|
|
||||||
int h = Math.Max(1, (int)Math.Round(img.Height * scale));
|
|
||||||
using (Bitmap cur = (w == img.Width && h == img.Height) ? new Bitmap(img) : new Bitmap(img, w, h))
|
|
||||||
{
|
|
||||||
foreach (long q in new long[] { 90, 75, 60 })
|
|
||||||
{
|
|
||||||
last = EncodeJpeg(cur, q);
|
|
||||||
// base64 字符数≈UTF-8 字节数;3.9M 留余量防 4M 硬限被卡
|
|
||||||
if (last.Length <= 3900000) return last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scale *= 0.8f;
|
|
||||||
}
|
|
||||||
return last;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string EncodeJpeg(Bitmap bmp, long quality)
|
|
||||||
{
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
var codec = GetJpegEncoder();
|
|
||||||
if (codec != null)
|
|
||||||
{
|
|
||||||
using (var ep = new EncoderParameters(1))
|
|
||||||
{
|
|
||||||
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
|
|
||||||
bmp.Save(ms, codec, ep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else bmp.Save(ms, ImageFormat.Jpeg);
|
|
||||||
return Convert.ToBase64String(ms.ToArray());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Base64 的 URI 表单转义:仅 + / = 三个字符转 %XX,字母数字无需转义。
|
|
||||||
/// 不能用 Uri.EscapeDataString:.NET Framework 对约 32K 以上字符串抛"URI 字符串太长"。</summary>
|
|
||||||
private static string EscapeBase64(string s)
|
|
||||||
{
|
|
||||||
var sb = new StringBuilder(s.Length + s.Length / 2);
|
|
||||||
for (int i = 0; i < s.Length; i++)
|
|
||||||
{
|
|
||||||
char c = s[i];
|
|
||||||
if (c == '+') sb.Append("%2B");
|
|
||||||
else if (c == '/') sb.Append("%2F");
|
|
||||||
else if (c == '=') sb.Append("%3D");
|
|
||||||
else sb.Append(c);
|
|
||||||
}
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImageCodecInfo GetJpegEncoder()
|
|
||||||
{
|
|
||||||
foreach (var c in ImageCodecInfo.GetImageEncoders())
|
|
||||||
if (c.FormatID == ImageFormat.Jpeg.Guid) return c;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() { }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Generated
-303
@@ -1,303 +0,0 @@
|
|||||||
namespace pdfbt
|
|
||||||
{
|
|
||||||
/// <summary>OCR 设置对话框:引擎选择(百度/智谱GLM) + 对应 Key(存 exe 旁 baidu_ocr.json)。</summary>
|
|
||||||
public partial class OcrSettingsForm : System.Windows.Forms.Form
|
|
||||||
{
|
|
||||||
/// <summary>必需的设计器变量。</summary>
|
|
||||||
private System.ComponentModel.IContainer components = null;
|
|
||||||
|
|
||||||
/// <summary>清理正在使用的资源。</summary>
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing && (components != null))
|
|
||||||
{
|
|
||||||
components.Dispose();
|
|
||||||
}
|
|
||||||
base.Dispose(disposing);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Windows 窗体设计器生成的代码
|
|
||||||
|
|
||||||
private void InitializeComponent()
|
|
||||||
{
|
|
||||||
this.lblEngine = new System.Windows.Forms.Label();
|
|
||||||
this.cboEngine = new System.Windows.Forms.ComboBox();
|
|
||||||
this.lblApiKey = new System.Windows.Forms.Label();
|
|
||||||
this.txtApiKey = new System.Windows.Forms.TextBox();
|
|
||||||
this.lblSecretKey = new System.Windows.Forms.Label();
|
|
||||||
this.txtSecretKey = new System.Windows.Forms.TextBox();
|
|
||||||
this.lblGlmKey = new System.Windows.Forms.Label();
|
|
||||||
this.txtGlmKey = new System.Windows.Forms.TextBox();
|
|
||||||
this.lblQianfanKey = new System.Windows.Forms.Label();
|
|
||||||
this.txtQianfanKey = new System.Windows.Forms.TextBox();
|
|
||||||
this.lblUapisKey = new System.Windows.Forms.Label();
|
|
||||||
this.txtUapisKey = new System.Windows.Forms.TextBox();
|
|
||||||
this.lblUmiUrl = new System.Windows.Forms.Label();
|
|
||||||
this.txtUmiUrl = new System.Windows.Forms.TextBox();
|
|
||||||
this.btnUmi下载 = new System.Windows.Forms.Button();
|
|
||||||
this.btnUmi检测 = new System.Windows.Forms.Button();
|
|
||||||
this.lblUmi状态 = new System.Windows.Forms.Label();
|
|
||||||
this.btn帮助 = new System.Windows.Forms.Button();
|
|
||||||
this.lbl说明 = new System.Windows.Forms.Label();
|
|
||||||
this.btn确定 = new System.Windows.Forms.Button();
|
|
||||||
this.btn取消 = new System.Windows.Forms.Button();
|
|
||||||
this.SuspendLayout();
|
|
||||||
//
|
|
||||||
// lblEngine
|
|
||||||
//
|
|
||||||
this.lblEngine.AutoSize = true;
|
|
||||||
this.lblEngine.Location = new System.Drawing.Point(12, 18);
|
|
||||||
this.lblEngine.Name = "lblEngine";
|
|
||||||
this.lblEngine.Size = new System.Drawing.Size(89, 18);
|
|
||||||
this.lblEngine.TabIndex = 0;
|
|
||||||
this.lblEngine.Text = "识别引擎:";
|
|
||||||
//
|
|
||||||
// cboEngine
|
|
||||||
//
|
|
||||||
this.cboEngine.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
|
||||||
this.cboEngine.Items.AddRange(new object[] {
|
|
||||||
"百度 OCR 高精度(含位置)",
|
|
||||||
"智谱 GLM-OCR(版面解析/手写强)",
|
|
||||||
"百度 PaddleOCR-VL(千帆版面解析)",
|
|
||||||
"UApiPro 通用OCR(免费积分/带位置)",
|
|
||||||
"Umi-OCR 本地引擎(离线免费,需先运行Umi-OCR)"});
|
|
||||||
this.cboEngine.Location = new System.Drawing.Point(139, 15);
|
|
||||||
this.cboEngine.Name = "cboEngine";
|
|
||||||
this.cboEngine.Size = new System.Drawing.Size(389, 26);
|
|
||||||
this.cboEngine.TabIndex = 1;
|
|
||||||
this.cboEngine.SelectedIndexChanged += new System.EventHandler(this.cboEngine_SelectedIndexChanged);
|
|
||||||
//
|
|
||||||
// lblApiKey
|
|
||||||
//
|
|
||||||
this.lblApiKey.AutoSize = true;
|
|
||||||
this.lblApiKey.Location = new System.Drawing.Point(12, 55);
|
|
||||||
this.lblApiKey.Name = "lblApiKey";
|
|
||||||
this.lblApiKey.Size = new System.Drawing.Size(80, 18);
|
|
||||||
this.lblApiKey.TabIndex = 2;
|
|
||||||
this.lblApiKey.Text = "API Key:";
|
|
||||||
//
|
|
||||||
// txtApiKey
|
|
||||||
//
|
|
||||||
this.txtApiKey.Location = new System.Drawing.Point(139, 52);
|
|
||||||
this.txtApiKey.Name = "txtApiKey";
|
|
||||||
this.txtApiKey.Size = new System.Drawing.Size(389, 28);
|
|
||||||
this.txtApiKey.TabIndex = 3;
|
|
||||||
this.txtApiKey.Text = "GHcn5qFbKpnScsrdyHWS8w0A";
|
|
||||||
this.txtApiKey.UseSystemPasswordChar = true;
|
|
||||||
//
|
|
||||||
// lblSecretKey
|
|
||||||
//
|
|
||||||
this.lblSecretKey.AutoSize = true;
|
|
||||||
this.lblSecretKey.Location = new System.Drawing.Point(12, 89);
|
|
||||||
this.lblSecretKey.Name = "lblSecretKey";
|
|
||||||
this.lblSecretKey.Size = new System.Drawing.Size(107, 18);
|
|
||||||
this.lblSecretKey.TabIndex = 4;
|
|
||||||
this.lblSecretKey.Text = "Secret Key:";
|
|
||||||
//
|
|
||||||
// txtSecretKey
|
|
||||||
//
|
|
||||||
this.txtSecretKey.Location = new System.Drawing.Point(139, 89);
|
|
||||||
this.txtSecretKey.Name = "txtSecretKey";
|
|
||||||
this.txtSecretKey.Size = new System.Drawing.Size(389, 28);
|
|
||||||
this.txtSecretKey.TabIndex = 5;
|
|
||||||
this.txtSecretKey.Text = "VC1UU6Eyq7o0pdK4IBgP8fsaZ22Cte1c";
|
|
||||||
this.txtSecretKey.UseSystemPasswordChar = true;
|
|
||||||
//
|
|
||||||
// lblGlmKey
|
|
||||||
//
|
|
||||||
this.lblGlmKey.AutoSize = true;
|
|
||||||
this.lblGlmKey.Location = new System.Drawing.Point(12, 55);
|
|
||||||
this.lblGlmKey.Name = "lblGlmKey";
|
|
||||||
this.lblGlmKey.Size = new System.Drawing.Size(125, 18);
|
|
||||||
this.lblGlmKey.TabIndex = 2;
|
|
||||||
this.lblGlmKey.Text = "智谱 API Key:";
|
|
||||||
//
|
|
||||||
// txtGlmKey
|
|
||||||
//
|
|
||||||
this.txtGlmKey.Location = new System.Drawing.Point(139, 52);
|
|
||||||
this.txtGlmKey.Name = "txtGlmKey";
|
|
||||||
this.txtGlmKey.Size = new System.Drawing.Size(389, 28);
|
|
||||||
this.txtGlmKey.TabIndex = 3;
|
|
||||||
this.txtGlmKey.UseSystemPasswordChar = true;
|
|
||||||
//
|
|
||||||
// lblQianfanKey
|
|
||||||
//
|
|
||||||
this.lblQianfanKey.AutoSize = true;
|
|
||||||
this.lblQianfanKey.Location = new System.Drawing.Point(12, 55);
|
|
||||||
this.lblQianfanKey.Name = "lblQianfanKey";
|
|
||||||
this.lblQianfanKey.Size = new System.Drawing.Size(125, 18);
|
|
||||||
this.lblQianfanKey.TabIndex = 2;
|
|
||||||
this.lblQianfanKey.Text = "千帆 API Key:";
|
|
||||||
//
|
|
||||||
// txtQianfanKey
|
|
||||||
//
|
|
||||||
this.txtQianfanKey.Location = new System.Drawing.Point(139, 52);
|
|
||||||
this.txtQianfanKey.Name = "txtQianfanKey";
|
|
||||||
this.txtQianfanKey.Size = new System.Drawing.Size(389, 28);
|
|
||||||
this.txtQianfanKey.TabIndex = 3;
|
|
||||||
this.txtQianfanKey.UseSystemPasswordChar = true;
|
|
||||||
//
|
|
||||||
// lblUapisKey
|
|
||||||
//
|
|
||||||
this.lblUapisKey.AutoSize = true;
|
|
||||||
this.lblUapisKey.Location = new System.Drawing.Point(12, 55);
|
|
||||||
this.lblUapisKey.Name = "lblUapisKey";
|
|
||||||
this.lblUapisKey.Size = new System.Drawing.Size(116, 18);
|
|
||||||
this.lblUapisKey.TabIndex = 2;
|
|
||||||
this.lblUapisKey.Text = "UApiPro Key:";
|
|
||||||
//
|
|
||||||
// txtUapisKey
|
|
||||||
//
|
|
||||||
this.txtUapisKey.Location = new System.Drawing.Point(139, 52);
|
|
||||||
this.txtUapisKey.Name = "txtUapisKey";
|
|
||||||
this.txtUapisKey.Size = new System.Drawing.Size(389, 28);
|
|
||||||
this.txtUapisKey.TabIndex = 3;
|
|
||||||
this.txtUapisKey.UseSystemPasswordChar = true;
|
|
||||||
//
|
|
||||||
// lblUmiUrl
|
|
||||||
//
|
|
||||||
this.lblUmiUrl.AutoSize = true;
|
|
||||||
this.lblUmiUrl.Location = new System.Drawing.Point(12, 55);
|
|
||||||
this.lblUmiUrl.Name = "lblUmiUrl";
|
|
||||||
this.lblUmiUrl.Size = new System.Drawing.Size(89, 18);
|
|
||||||
this.lblUmiUrl.TabIndex = 2;
|
|
||||||
this.lblUmiUrl.Text = "服务地址:";
|
|
||||||
//
|
|
||||||
// txtUmiUrl
|
|
||||||
//
|
|
||||||
this.txtUmiUrl.Location = new System.Drawing.Point(139, 52);
|
|
||||||
this.txtUmiUrl.Name = "txtUmiUrl";
|
|
||||||
this.txtUmiUrl.Size = new System.Drawing.Size(389, 28);
|
|
||||||
this.txtUmiUrl.TabIndex = 3;
|
|
||||||
this.txtUmiUrl.Text = "http://127.0.0.1:1224";
|
|
||||||
//
|
|
||||||
// btnUmi下载
|
|
||||||
//
|
|
||||||
this.btnUmi下载.Location = new System.Drawing.Point(139, 86);
|
|
||||||
this.btnUmi下载.Name = "btnUmi下载";
|
|
||||||
this.btnUmi下载.Size = new System.Drawing.Size(139, 28);
|
|
||||||
this.btnUmi下载.TabIndex = 4;
|
|
||||||
this.btnUmi下载.Text = "下载 OCR 本体";
|
|
||||||
this.btnUmi下载.UseVisualStyleBackColor = true;
|
|
||||||
this.btnUmi下载.Click += new System.EventHandler(this.btnUmi下载_Click);
|
|
||||||
//
|
|
||||||
// btnUmi检测
|
|
||||||
//
|
|
||||||
this.btnUmi检测.Location = new System.Drawing.Point(304, 86);
|
|
||||||
this.btnUmi检测.Name = "btnUmi检测";
|
|
||||||
this.btnUmi检测.Size = new System.Drawing.Size(138, 28);
|
|
||||||
this.btnUmi检测.TabIndex = 5;
|
|
||||||
this.btnUmi检测.Text = "检测 OCR 本体";
|
|
||||||
this.btnUmi检测.UseVisualStyleBackColor = true;
|
|
||||||
this.btnUmi检测.Click += new System.EventHandler(this.btnUmi检测_Click);
|
|
||||||
//
|
|
||||||
// lblUmi状态
|
|
||||||
//
|
|
||||||
this.lblUmi状态.ForeColor = System.Drawing.SystemColors.GrayText;
|
|
||||||
this.lblUmi状态.Location = new System.Drawing.Point(12, 118);
|
|
||||||
this.lblUmi状态.Name = "lblUmi状态";
|
|
||||||
this.lblUmi状态.Size = new System.Drawing.Size(506, 20);
|
|
||||||
this.lblUmi状态.TabIndex = 6;
|
|
||||||
//
|
|
||||||
// btn帮助
|
|
||||||
//
|
|
||||||
this.btn帮助.Location = new System.Drawing.Point(12, 200);
|
|
||||||
this.btn帮助.Name = "btn帮助";
|
|
||||||
this.btn帮助.Size = new System.Drawing.Size(90, 28);
|
|
||||||
this.btn帮助.TabIndex = 10;
|
|
||||||
this.btn帮助.Text = "帮助";
|
|
||||||
this.btn帮助.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// lbl说明
|
|
||||||
//
|
|
||||||
this.lbl说明.ForeColor = System.Drawing.SystemColors.GrayText;
|
|
||||||
this.lbl说明.Location = new System.Drawing.Point(12, 142);
|
|
||||||
this.lbl说明.Name = "lbl说明";
|
|
||||||
this.lbl说明.Size = new System.Drawing.Size(506, 46);
|
|
||||||
this.lbl说明.TabIndex = 7;
|
|
||||||
this.lbl说明.Text = "百度引擎已内置默认 Key,可直接使用;换自己的 Key 更稳。\r\n智谱 GLM-OCR(手写字/版面强)需到 open.bigmodel.cn 获取 API K" +
|
|
||||||
"ey,约 0.2 元/百万 tokens。\r\nUmi-OCR 为本机离线引擎(免费无限次):本体将从网盘自动下载到程序目录,识别时自动启动、关窗自动关闭。";
|
|
||||||
//
|
|
||||||
// btn确定
|
|
||||||
//
|
|
||||||
this.btn确定.DialogResult = System.Windows.Forms.DialogResult.OK;
|
|
||||||
this.btn确定.Location = new System.Drawing.Point(367, 200);
|
|
||||||
this.btn确定.Name = "btn确定";
|
|
||||||
this.btn确定.Size = new System.Drawing.Size(75, 28);
|
|
||||||
this.btn确定.TabIndex = 8;
|
|
||||||
this.btn确定.Text = "保存";
|
|
||||||
this.btn确定.UseVisualStyleBackColor = true;
|
|
||||||
this.btn确定.Click += new System.EventHandler(this.btn确定_Click);
|
|
||||||
//
|
|
||||||
// btn取消
|
|
||||||
//
|
|
||||||
this.btn取消.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
|
||||||
this.btn取消.Location = new System.Drawing.Point(449, 200);
|
|
||||||
this.btn取消.Name = "btn取消";
|
|
||||||
this.btn取消.Size = new System.Drawing.Size(75, 28);
|
|
||||||
this.btn取消.TabIndex = 9;
|
|
||||||
this.btn取消.Text = "取消";
|
|
||||||
this.btn取消.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// OcrSettingsForm
|
|
||||||
//
|
|
||||||
this.AcceptButton = this.btn确定;
|
|
||||||
this.CancelButton = this.btn取消;
|
|
||||||
this.ClientSize = new System.Drawing.Size(539, 244);
|
|
||||||
this.Controls.Add(this.lblEngine);
|
|
||||||
this.Controls.Add(this.cboEngine);
|
|
||||||
this.Controls.Add(this.lblApiKey);
|
|
||||||
this.Controls.Add(this.txtApiKey);
|
|
||||||
this.Controls.Add(this.lblSecretKey);
|
|
||||||
this.Controls.Add(this.txtSecretKey);
|
|
||||||
this.Controls.Add(this.lblGlmKey);
|
|
||||||
this.Controls.Add(this.txtGlmKey);
|
|
||||||
this.Controls.Add(this.lblQianfanKey);
|
|
||||||
this.Controls.Add(this.txtQianfanKey);
|
|
||||||
this.Controls.Add(this.lblUapisKey);
|
|
||||||
this.Controls.Add(this.txtUapisKey);
|
|
||||||
this.Controls.Add(this.lblUmiUrl);
|
|
||||||
this.Controls.Add(this.txtUmiUrl);
|
|
||||||
this.Controls.Add(this.btnUmi下载);
|
|
||||||
this.Controls.Add(this.btnUmi检测);
|
|
||||||
this.Controls.Add(this.lblUmi状态);
|
|
||||||
this.Controls.Add(this.lbl说明);
|
|
||||||
this.Controls.Add(this.btn帮助);
|
|
||||||
this.Controls.Add(this.btn确定);
|
|
||||||
this.Controls.Add(this.btn取消);
|
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
|
||||||
this.MaximizeBox = false;
|
|
||||||
this.MinimizeBox = false;
|
|
||||||
this.Name = "OcrSettingsForm";
|
|
||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
|
||||||
this.Text = "OCR 设置";
|
|
||||||
this.ResumeLayout(false);
|
|
||||||
this.PerformLayout();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private System.Windows.Forms.Label lblEngine;
|
|
||||||
private System.Windows.Forms.ComboBox cboEngine;
|
|
||||||
private System.Windows.Forms.Label lblApiKey;
|
|
||||||
private System.Windows.Forms.TextBox txtApiKey;
|
|
||||||
private System.Windows.Forms.Label lblSecretKey;
|
|
||||||
private System.Windows.Forms.TextBox txtSecretKey;
|
|
||||||
private System.Windows.Forms.Label lblGlmKey;
|
|
||||||
private System.Windows.Forms.TextBox txtGlmKey;
|
|
||||||
private System.Windows.Forms.Label lblQianfanKey;
|
|
||||||
private System.Windows.Forms.TextBox txtQianfanKey;
|
|
||||||
private System.Windows.Forms.Label lblUapisKey;
|
|
||||||
private System.Windows.Forms.TextBox txtUapisKey;
|
|
||||||
private System.Windows.Forms.Label lblUmiUrl;
|
|
||||||
private System.Windows.Forms.TextBox txtUmiUrl;
|
|
||||||
private System.Windows.Forms.Button btnUmi下载;
|
|
||||||
private System.Windows.Forms.Button btnUmi检测;
|
|
||||||
private System.Windows.Forms.Label lblUmi状态;
|
|
||||||
private System.Windows.Forms.Button btn帮助;
|
|
||||||
private System.Windows.Forms.Label lbl说明;
|
|
||||||
private System.Windows.Forms.Button btn确定;
|
|
||||||
private System.Windows.Forms.Button btn取消;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
// OcrSettingsForm:OCR 引擎与 Key 设置(百度 OCR / 智谱 GLM-OCR / 千帆 / UApiPro / Umi-OCR 本地)
|
|
||||||
// 保存 → exe 旁 baidu_ocr.json(engine/api_key/secret_key/glm_api_key/qianfan_api_key/uapis_api_key/umi_url)
|
|
||||||
// + 热更新 OcrClient + 作废百度 token 缓存。
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using System.Web.Script.Serialization;
|
|
||||||
|
|
||||||
namespace pdfbt
|
|
||||||
{
|
|
||||||
public partial class OcrSettingsForm : Form
|
|
||||||
{
|
|
||||||
private readonly OcrClient _ocr;
|
|
||||||
|
|
||||||
public OcrSettingsForm(OcrClient ocr)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
_ocr = ocr;
|
|
||||||
|
|
||||||
// 回填现有配置(默认 Key 在 Designer 的 Text 里,密码遮罩显示;保存过则文件值优先)
|
|
||||||
string engine = "baidu";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string cfgPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json");
|
|
||||||
if (File.Exists(cfgPath))
|
|
||||||
{
|
|
||||||
var jsv = new JavaScriptSerializer().Deserialize<Dictionary<string, string>>(File.ReadAllText(cfgPath));
|
|
||||||
if (jsv != null)
|
|
||||||
{
|
|
||||||
if (jsv.ContainsKey("engine")) engine = jsv["engine"] ?? "baidu";
|
|
||||||
txtApiKey.Text = jsv.ContainsKey("api_key") ? (jsv["api_key"] ?? "") : "";
|
|
||||||
txtSecretKey.Text = jsv.ContainsKey("secret_key") ? (jsv["secret_key"] ?? "") : "";
|
|
||||||
txtGlmKey.Text = jsv.ContainsKey("glm_api_key") ? (jsv["glm_api_key"] ?? "") : "";
|
|
||||||
txtQianfanKey.Text = jsv.ContainsKey("qianfan_api_key") ? (jsv["qianfan_api_key"] ?? "") : "";
|
|
||||||
txtUapisKey.Text = jsv.ContainsKey("uapis_api_key") ? (jsv["uapis_api_key"] ?? "") : "";
|
|
||||||
txtUmiUrl.Text = jsv.ContainsKey("umi_url") && !string.IsNullOrWhiteSpace(jsv["umi_url"])
|
|
||||||
? jsv["umi_url"] : "http://127.0.0.1:1224";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
|
|
||||||
cboEngine.SelectedIndex =
|
|
||||||
engine == "glm" ? 1 :
|
|
||||||
engine == "qianfan" ? 2 :
|
|
||||||
engine == "uapis" ? 3 :
|
|
||||||
engine == "umi" ? 4 : 0;
|
|
||||||
UpdateEnginePanels();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>按引擎下拉显示对应的输入区(百度=两格;智谱/千帆/UApiPro=Key一格;Umi-OCR=服务地址一格)。</summary>
|
|
||||||
private void UpdateEnginePanels()
|
|
||||||
{
|
|
||||||
int sel = cboEngine.SelectedIndex;
|
|
||||||
bool glm = sel == 1;
|
|
||||||
bool qf = sel == 2;
|
|
||||||
bool ua = sel == 3;
|
|
||||||
bool umi = sel == 4;
|
|
||||||
bool baidu = !glm && !qf && !ua && !umi;
|
|
||||||
lblApiKey.Visible = baidu;
|
|
||||||
txtApiKey.Visible = baidu;
|
|
||||||
lblSecretKey.Visible = baidu;
|
|
||||||
txtSecretKey.Visible = baidu;
|
|
||||||
lblGlmKey.Visible = glm;
|
|
||||||
txtGlmKey.Visible = glm;
|
|
||||||
lblQianfanKey.Visible = qf;
|
|
||||||
txtQianfanKey.Visible = qf;
|
|
||||||
lblUapisKey.Visible = ua;
|
|
||||||
txtUapisKey.Visible = ua;
|
|
||||||
lblUmiUrl.Visible = umi;
|
|
||||||
txtUmiUrl.Visible = umi;
|
|
||||||
btnUmi下载.Visible = umi;
|
|
||||||
btnUmi检测.Visible = umi;
|
|
||||||
lblUmi状态.Visible = umi;
|
|
||||||
if (umi) RefreshUmiStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>刷新 Umi 本体三态状态显示。</summary>
|
|
||||||
private void RefreshUmiStatus()
|
|
||||||
{
|
|
||||||
try { lblUmi状态.Text = UmiOcrHost.DetectText(); } catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>检测 OCR 本体:刷新状态(未安装/已安装未运行/运行中)。</summary>
|
|
||||||
private void btnUmi检测_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
RefreshUmiStatus();
|
|
||||||
MessageBox.Show(this, UmiOcrHost.DetectText(), "检测 OCR 本体",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>下载 OCR 本体:网盘直链(~179MB)→解压到程序目录 Umi-OCR\→验证→删 zip。</summary>
|
|
||||||
private void btnUmi下载_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (UmiOcrHost.IsInstalled)
|
|
||||||
{
|
|
||||||
var r = MessageBox.Show(this, "OCR 本体已存在,重新下载将覆盖安装。继续吗?", "下载 OCR 本体",
|
|
||||||
MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
|
|
||||||
if (r != DialogResult.OK) return;
|
|
||||||
}
|
|
||||||
btnUmi下载.Enabled = false;
|
|
||||||
btnUmi检测.Enabled = false;
|
|
||||||
lblUmi状态.ForeColor = System.Drawing.Color.DimGray;
|
|
||||||
lblUmi状态.Text = "正在下载… 0%";
|
|
||||||
int lastPct = -1;
|
|
||||||
UmiOcrHost.DownloadAndInstallAsync(pct =>
|
|
||||||
{
|
|
||||||
if (pct == lastPct) return;
|
|
||||||
lastPct = pct;
|
|
||||||
try { this.BeginInvoke((MethodInvoker)delegate
|
|
||||||
{
|
|
||||||
lblUmi状态.Text = pct >= 100 ? "下载完成,正在解压安装…" : "正在下载… " + pct + "%";
|
|
||||||
}); } catch { }
|
|
||||||
}).ContinueWith(t =>
|
|
||||||
{
|
|
||||||
try { this.BeginInvoke((MethodInvoker)delegate
|
|
||||||
{
|
|
||||||
btnUmi下载.Enabled = true;
|
|
||||||
btnUmi检测.Enabled = true;
|
|
||||||
lblUmi状态.ForeColor = System.Drawing.SystemColors.GrayText;
|
|
||||||
if (t.IsFaulted)
|
|
||||||
{
|
|
||||||
Exception root = t.Exception;
|
|
||||||
while (root is AggregateException && root.InnerException != null) root = root.InnerException;
|
|
||||||
lblUmi状态.Text = "下载/安装失败: " + root.Message;
|
|
||||||
MessageBox.Show(this, "下载/安装失败:\n" + root.Message, "下载 OCR 本体",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RefreshUmiStatus();
|
|
||||||
MessageBox.Show(this, "OCR 本体安装完成!\n" + UmiOcrHost.DetectText() +
|
|
||||||
"\n\n选 Umi-OCR 引擎保存后即可使用(识别时自动启动)。",
|
|
||||||
"下载 OCR 本体", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
}
|
|
||||||
}); } catch { }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cboEngine_SelectedIndexChanged(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
UpdateEnginePanels();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>帮助:全部引擎的 Key 获取方式与本地 OCR 设置流程。</summary>
|
|
||||||
private void btn帮助_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
string help =
|
|
||||||
"════════ OCR 识别引擎使用帮助 ════════\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"【1. 百度 OCR 高精度(含位置)】 默认引擎,已内置 Key 可直接用\r\n" +
|
|
||||||
" · 获取自己的 Key(更稳,免费额度足够日常):\r\n" +
|
|
||||||
" ① 打开 console.baidu.com 登录百度账号\r\n" +
|
|
||||||
" ② 进入\"产品服务 / 文字识别\"→ 创建应用\r\n" +
|
|
||||||
" ③ 勾选\"文字识别\"全部接口,创建完成\r\n" +
|
|
||||||
" ④ 在应用详情里复制 API Key 和 Secret Key 填入本窗口\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"【2. 智谱 GLM-OCR(版面解析/手写强)】\r\n" +
|
|
||||||
" · 到 open.bigmodel.cn 注册并实名 → API Keys 页面新建密钥\r\n" +
|
|
||||||
" · 把密钥填入\"智谱 API Key\"\r\n" +
|
|
||||||
" · 计费约 0.2 元/百万 tokens,适合手写体/复杂版面\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"【3. 百度 PaddleOCR-VL(千帆版面解析)】\r\n" +
|
|
||||||
" · 到 console.bce.baidu.com → 千帆 ModelBuilder → 应用管理\r\n" +
|
|
||||||
" · 创建应用后复制 API Key 填入\"千帆 API Key\"\r\n" +
|
|
||||||
" · 需在千帆控制台开通 PaddleOCR-VL 模型服务\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"【4. UApiPro 通用OCR(免费积分/带位置)】\r\n" +
|
|
||||||
" · 到 uapis.cn 注册账号 → 控制台 → API Keys → 新建\r\n" +
|
|
||||||
" · 把 Key 填入\"UApiPro Key\"\r\n" +
|
|
||||||
" · 注册送免费积分(约 4 积分/次识别),小量使用零成本\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"【5. Umi-OCR 本地引擎(离线免费,无限次)】\r\n" +
|
|
||||||
" · 本地引擎无需任何 Key,识别全在本机完成,数据不出电脑\r\n" +
|
|
||||||
" · 设置流程:\r\n" +
|
|
||||||
" ① 选本引擎后点\"下载 OCR 本体\"(约 179MB,自动下载安装)\r\n" +
|
|
||||||
" ② 点\"检测 OCR 本体\"确认状态为\"运行中\"或\"已安装\"\r\n" +
|
|
||||||
" ③ 点\"保存\"完成,之后识别时自动启动、关窗自动关闭\r\n" +
|
|
||||||
" · 服务地址保持默认 http://127.0.0.1:1224 即可\r\n" +
|
|
||||||
" (只有你在 Umi-OCR 软件里改过 HTTP 端口才需要改这里)\r\n" +
|
|
||||||
"\r\n" +
|
|
||||||
"════════════════════════════════\r\n" +
|
|
||||||
"提示: Key 全部以密码方式显示,点\"保存\"后立即生效;\r\n" +
|
|
||||||
"所有 Key 统一保存在程序目录 baidu_ocr.json。";
|
|
||||||
MessageBox.Show(this, help, "OCR 使用帮助",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void btn确定_Click(object sender, EventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string cfgPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json");
|
|
||||||
string engineValue =
|
|
||||||
cboEngine.SelectedIndex == 1 ? "glm" :
|
|
||||||
cboEngine.SelectedIndex == 2 ? "qianfan" :
|
|
||||||
cboEngine.SelectedIndex == 3 ? "uapis" :
|
|
||||||
cboEngine.SelectedIndex == 4 ? "umi" : "baidu";
|
|
||||||
var jsv = new JavaScriptSerializer();
|
|
||||||
File.WriteAllText(cfgPath, jsv.Serialize(new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "engine", engineValue },
|
|
||||||
{ "api_key", txtApiKey.Text.Trim() },
|
|
||||||
{ "secret_key", txtSecretKey.Text.Trim() },
|
|
||||||
{ "glm_api_key", txtGlmKey.Text.Trim() },
|
|
||||||
{ "qianfan_api_key", txtQianfanKey.Text.Trim() },
|
|
||||||
{ "uapis_api_key", txtUapisKey.Text.Trim() },
|
|
||||||
{ "umi_url", string.IsNullOrWhiteSpace(txtUmiUrl.Text.Trim()) ? "http://127.0.0.1:1224" : txtUmiUrl.Text.Trim() }
|
|
||||||
}));
|
|
||||||
// 热更新引擎/Key + 作废百度 token 缓存(下次识别重新换取)
|
|
||||||
if (_ocr != null)
|
|
||||||
{
|
|
||||||
_ocr.LoadConfig();
|
|
||||||
try { File.Delete(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_token.json")); } catch { }
|
|
||||||
}
|
|
||||||
// 选了 Umi-OCR 但还没装本体:提示引导下载(不阻断保存)
|
|
||||||
if (engineValue == "umi" && !UmiOcrHost.IsInstalled)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "Umi-OCR 本体尚未安装。\n请点\"下载 OCR 本体\"完成安装后即可使用。",
|
|
||||||
"OCR 设置", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
MessageBox.Show(this, "保存失败: " + ex.Message, "OCR 设置",
|
|
||||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<root>
|
|
||||||
<!--
|
|
||||||
Microsoft ResX Schema
|
|
||||||
|
|
||||||
Version 2.0
|
|
||||||
|
|
||||||
The primary goals of this format is to allow a simple XML format
|
|
||||||
that is mostly human readable. The generation and parsing of the
|
|
||||||
various data types are done through the TypeConverter classes
|
|
||||||
associated with the data types.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
... ado.net/XML headers & schema ...
|
|
||||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
|
||||||
<resheader name="version">2.0</resheader>
|
|
||||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
|
||||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
|
||||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
|
||||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
|
||||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
|
||||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
|
||||||
</data>
|
|
||||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
|
||||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
|
||||||
<comment>This is a comment</comment>
|
|
||||||
</data>
|
|
||||||
|
|
||||||
There are any number of "resheader" rows that contain simple
|
|
||||||
name/value pairs.
|
|
||||||
|
|
||||||
Each data row contains a name, and value. The row also contains a
|
|
||||||
type or mimetype. Type corresponds to a .NET class that support
|
|
||||||
text/value conversion through the TypeConverter architecture.
|
|
||||||
Classes that don't support this are serialized and stored with the
|
|
||||||
mimetype set.
|
|
||||||
|
|
||||||
The mimetype is used for serialized objects, and tells the
|
|
||||||
ResXResourceReader how to depersist the object. This is currently not
|
|
||||||
extensible. For a given mimetype the value must be set accordingly:
|
|
||||||
|
|
||||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
|
||||||
that the ResXResourceWriter will generate, however the reader can
|
|
||||||
read any of the formats listed below.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.binary.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.soap.base64
|
|
||||||
value : The object must be serialized with
|
|
||||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
|
|
||||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
|
||||||
value : The object must be serialized into a byte array
|
|
||||||
: using a System.ComponentModel.TypeConverter
|
|
||||||
: and then encoded with base64 encoding.
|
|
||||||
-->
|
|
||||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
|
||||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
|
||||||
<xsd:element name="root" msdata:IsDataSet="true">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:choice maxOccurs="unbounded">
|
|
||||||
<xsd:element name="metadata">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="assembly">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:attribute name="alias" type="xsd:string" />
|
|
||||||
<xsd:attribute name="name" type="xsd:string" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="data">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
|
||||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
|
||||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
|
||||||
<xsd:attribute ref="xml:space" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
<xsd:element name="resheader">
|
|
||||||
<xsd:complexType>
|
|
||||||
<xsd:sequence>
|
|
||||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
|
||||||
</xsd:sequence>
|
|
||||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:choice>
|
|
||||||
</xsd:complexType>
|
|
||||||
</xsd:element>
|
|
||||||
</xsd:schema>
|
|
||||||
<resheader name="resmimetype">
|
|
||||||
<value>text/microsoft-resx</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="version">
|
|
||||||
<value>2.0</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="reader">
|
|
||||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
<resheader name="writer">
|
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
|
||||||
</resheader>
|
|
||||||
</root>
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// PdfDocument:PDF 文档封装(加载/页尺寸/按缩放渲染页位图)
|
|
||||||
// 坐标系:页面坐标 = PDF 点(1/72 英寸),原点左上(pdfium 渲染后
|
|
||||||
// 已翻转为屏幕方向,y 向下)。
|
|
||||||
// ============================================================
|
|
||||||
using System;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace pdfbt
|
|
||||||
{
|
|
||||||
/// <summary>已打开的 PDF 文档。非线程安全,只在 UI 线程使用。</summary>
|
|
||||||
internal sealed class PdfDocument : IDisposable
|
|
||||||
{
|
|
||||||
private IntPtr _doc = IntPtr.Zero;
|
|
||||||
private IntPtr[] _pageHandles; // 惰性加载的页句柄缓存
|
|
||||||
// ★ FPDF_LoadMemDocument 要求后备缓冲在文档关闭前保持有效且不可移动——
|
|
||||||
// 原来读入的 byte[] 是局部变量,打开后就没人引用;打开下一个 PDF 的大分配
|
|
||||||
// 触发 GC 把它搬走/回收,文档内存变垃圾 → 第二个 PDF 打不开(实测用户报障)。
|
|
||||||
// 钉住(GCHandleType.Pinned)+字段保引用,Close 时才释放。
|
|
||||||
private byte[] _bytes;
|
|
||||||
private System.Runtime.InteropServices.GCHandle _pin;
|
|
||||||
|
|
||||||
public string FilePath { get; private set; }
|
|
||||||
public int PageCount { get; private set; }
|
|
||||||
public float[] PageWidthPt { get; private set; }
|
|
||||||
public float[] PageHeightPt { get; private set; }
|
|
||||||
|
|
||||||
public bool IsOpen
|
|
||||||
{
|
|
||||||
get { return _doc != IntPtr.Zero; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>打开 PDF(读入内存后走 FPDF_LoadMemDocument,免疫路径问题)。失败抛异常。</summary>
|
|
||||||
public void Open(string path, string password)
|
|
||||||
{
|
|
||||||
Close();
|
|
||||||
if (!File.Exists(path))
|
|
||||||
throw new Exception("文件不存在: " + path);
|
|
||||||
_bytes = File.ReadAllBytes(path);
|
|
||||||
_pin = System.Runtime.InteropServices.GCHandle.Alloc(_bytes,
|
|
||||||
System.Runtime.InteropServices.GCHandleType.Pinned);
|
|
||||||
IntPtr doc = PdfiumNative.LoadDocumentFromBytes(_bytes, password);
|
|
||||||
if (doc == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
FreeBuffer();
|
|
||||||
throw new Exception("PDF 打开失败:" + PdfiumNative.LastErrorText());
|
|
||||||
}
|
|
||||||
|
|
||||||
int count = PdfiumNative.GetPageCount(doc);
|
|
||||||
if (count <= 0)
|
|
||||||
{
|
|
||||||
PdfiumNative.CloseDocument(doc);
|
|
||||||
FreeBuffer();
|
|
||||||
throw new Exception("PDF 没有任何页面。");
|
|
||||||
}
|
|
||||||
|
|
||||||
_doc = doc;
|
|
||||||
FilePath = path;
|
|
||||||
PageCount = count;
|
|
||||||
_pageHandles = new IntPtr[count];
|
|
||||||
PageWidthPt = new float[count];
|
|
||||||
PageHeightPt = new float[count];
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
float w, h;
|
|
||||||
if (!PdfiumNative.GetPageSize(doc, i, out w, out h))
|
|
||||||
{
|
|
||||||
w = 595f; h = 842f; // A4 兜底
|
|
||||||
}
|
|
||||||
PageWidthPt[i] = w;
|
|
||||||
PageHeightPt[i] = h;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>释放文档后备缓冲(钉住句柄+字节数组)。</summary>
|
|
||||||
private void FreeBuffer()
|
|
||||||
{
|
|
||||||
if (_pin.IsAllocated) _pin.Free();
|
|
||||||
_bytes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Close()
|
|
||||||
{
|
|
||||||
if (_pageHandles != null)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < _pageHandles.Length; i++)
|
|
||||||
PdfiumNative.ClosePage(_pageHandles[i]);
|
|
||||||
_pageHandles = null;
|
|
||||||
}
|
|
||||||
if (_doc != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
PdfiumNative.CloseDocument(_doc);
|
|
||||||
_doc = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
FreeBuffer();
|
|
||||||
FilePath = null;
|
|
||||||
PageCount = 0;
|
|
||||||
PageWidthPt = null;
|
|
||||||
PageHeightPt = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>渲染指定页。scale = 像素/PDF点。失败返回 null。</summary>
|
|
||||||
public Bitmap RenderPage(int pageIndex, float scale)
|
|
||||||
{
|
|
||||||
if (!IsOpen) return null;
|
|
||||||
if (pageIndex < 0 || pageIndex >= PageCount) return null;
|
|
||||||
IntPtr page = GetPage(pageIndex);
|
|
||||||
if (page == IntPtr.Zero) return null;
|
|
||||||
|
|
||||||
int w = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * scale));
|
|
||||||
int h = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * scale));
|
|
||||||
return PdfiumNative.RenderPageToBitmap(page, w, h);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 高清渲染页面局部区域(OCR 用):与屏幕显示缩放无关,
|
|
||||||
/// 用户任意缩放下框选,OCR 都拿到按 ocrScale 渲染的高清裁剪。
|
|
||||||
/// 返回位图与 rectPt 一一对应(像素/pt = ocrScale)。
|
|
||||||
/// </summary>
|
|
||||||
public Bitmap RenderRegion(int pageIndex, RectangleF rectPt, float ocrScale)
|
|
||||||
{
|
|
||||||
if (!IsOpen) return null;
|
|
||||||
if (pageIndex < 0 || pageIndex >= PageCount) return null;
|
|
||||||
IntPtr page = GetPage(pageIndex);
|
|
||||||
if (page == IntPtr.Zero) return null;
|
|
||||||
|
|
||||||
// 整页渲染尺寸(控制缩放)
|
|
||||||
int fullW = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * ocrScale));
|
|
||||||
int fullH = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * ocrScale));
|
|
||||||
// 区域矩形(裁到页面范围)
|
|
||||||
float pw = PageWidthPt[pageIndex], ph = PageHeightPt[pageIndex];
|
|
||||||
float l = Math.Max(0, rectPt.X), t = Math.Max(0, rectPt.Y);
|
|
||||||
float r = Math.Min(pw, rectPt.Right), b = Math.Min(ph, rectPt.Bottom);
|
|
||||||
if (r - l < 1 || b - t < 1) return null;
|
|
||||||
int startX = (int)Math.Floor(l * ocrScale);
|
|
||||||
int startY = (int)Math.Floor(t * ocrScale);
|
|
||||||
int endX = (int)Math.Ceiling(r * ocrScale);
|
|
||||||
int endY = (int)Math.Ceiling(b * ocrScale);
|
|
||||||
return PdfiumNative.RenderRegionToBitmap(page, startX, startY,
|
|
||||||
endX - startX, endY - startY, fullW, fullH);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IntPtr GetPage(int pageIndex)
|
|
||||||
{
|
|
||||||
IntPtr page = _pageHandles[pageIndex];
|
|
||||||
if (page == IntPtr.Zero)
|
|
||||||
{
|
|
||||||
page = PdfiumNative.LoadPage(_doc, pageIndex);
|
|
||||||
_pageHandles[pageIndex] = page;
|
|
||||||
}
|
|
||||||
return page;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web.Script.Serialization;
|
using System.Web.Script.Serialization;
|
||||||
|
using 成套报价软件; // ★ A1:OCR/PDF 基础设施已链接共享主项目源码(类型在 成套报价软件 命名空间)
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using WeifenLuo.WinFormsUI.Docking;
|
using WeifenLuo.WinFormsUI.Docking;
|
||||||
|
|
||||||
@@ -723,7 +724,7 @@ namespace pdfbt
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_statusLeft.Text = "正在启动 Umi-OCR 本地引擎…";
|
_statusLeft.Text = "正在启动 Umi-OCR 本地引擎…";
|
||||||
if (!UmiOcrHost.EnsureRunning())
|
if (!UmiOcrLauncher.EnsureRunning()) // ★ A1:启停已拆到 UmiOcrLauncher(主项目共享版)
|
||||||
{
|
{
|
||||||
_statusLeft.Text = "Umi-OCR 服务启动超时(本体已安装),请稍后重试或检查 Umi-OCR 窗口";
|
_statusLeft.Text = "Umi-OCR 服务启动超时(本体已安装),请稍后重试或检查 Umi-OCR 窗口";
|
||||||
crop.Dispose();
|
crop.Dispose();
|
||||||
@@ -1268,7 +1269,7 @@ namespace pdfbt
|
|||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
// Umi-OCR:只停由本程序启动的实例(用户自己开的 Umi-OCR 不动)
|
// Umi-OCR:只停由本程序启动的实例(用户自己开的 Umi-OCR 不动)
|
||||||
try { UmiOcrHost.StopIfStartedByUs(); } catch { }
|
try { UmiOcrLauncher.StopIfStartedByUs(); } catch { } // ★ A1:启停已拆到 UmiOcrLauncher(主项目共享版)
|
||||||
}
|
}
|
||||||
base.Dispose(disposing);
|
base.Dispose(disposing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,228 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// PdfiumNative:pdfium.dll 原生库 P/Invoke 封装
|
|
||||||
// dll 来源:pypdfium2 win32 wheel(pypdfium2_raw/pdfium.dll,x86)
|
|
||||||
// 调用约定:cdecl(pypdfium2 ctypes.CDLL 已验证)
|
|
||||||
// ============================================================
|
|
||||||
using System;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace pdfbt
|
|
||||||
{
|
|
||||||
/// <summary>pdfium C API 最小封装(仅本项目用到的函数)。</summary>
|
|
||||||
internal static class PdfiumNative
|
|
||||||
{
|
|
||||||
public const string DllName = "pdfium.dll";
|
|
||||||
|
|
||||||
// 渲染标志:渲染注释/批注
|
|
||||||
public const int FPDF_ANNOT = 0x01;
|
|
||||||
|
|
||||||
// FPDF_ERR_* 错误码(GetLastError 返回)
|
|
||||||
public const int FPDF_ERR_SUCCESS = 0; // 无错误
|
|
||||||
public const int FPDF_ERR_UNKNOWN = 1; // 未知错误
|
|
||||||
public const int FPDF_ERR_FILE = 2; // 文件无法打开或读取
|
|
||||||
public const int FPDF_ERR_FORMAT = 3; // 文件不是 PDF 或已损坏
|
|
||||||
public const int FPDF_ERR_PASSWORD = 4; // 需要密码或密码错误
|
|
||||||
public const int FPDF_ERR_SECURITY = 5; // 不支持的安全方案
|
|
||||||
public const int FPDF_ERR_PAGE = 6; // 页面无法加载
|
|
||||||
|
|
||||||
private static bool _inited;
|
|
||||||
private static readonly object _initLock = new object();
|
|
||||||
|
|
||||||
/// <summary>初始化 pdfium(进程内只需一次,线程安全)。</summary>
|
|
||||||
public static void InitLibrary()
|
|
||||||
{
|
|
||||||
lock (_initLock)
|
|
||||||
{
|
|
||||||
if (_inited) return;
|
|
||||||
FPDF_InitLibrary();
|
|
||||||
_inited = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDF_InitLibrary();
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDF_DestroyLibrary();
|
|
||||||
|
|
||||||
[DllImport(DllName, CharSet = CharSet.Ansi)]
|
|
||||||
private static extern IntPtr FPDF_LoadDocument(string file_path, string password);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDF_CloseDocument(IntPtr document);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern uint FPDF_GetLastError();
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern int FPDF_GetPageCount(IntPtr document);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern IntPtr FPDF_LoadPage(IntPtr document, int page_index);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDF_ClosePage(IntPtr page);
|
|
||||||
|
|
||||||
// ★ 新版 GetPageSizeByIndexF 是 FS_SIZEF* 参数(易错);用稳定的旧版 double API
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern int FPDF_GetPageSizeByIndex(IntPtr document, int page_index,
|
|
||||||
out double width, out double height);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern IntPtr FPDFBitmap_Create(int width, int height, int alpha);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDFBitmap_FillRect(IntPtr bitmap, int left, int top,
|
|
||||||
int width, int height, uint color);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDFBitmap_Destroy(IntPtr bitmap);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern IntPtr FPDFBitmap_GetBuffer(IntPtr bitmap);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern int FPDFBitmap_GetStride(IntPtr bitmap);
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern void FPDF_RenderPageBitmap(IntPtr bitmap, IntPtr page,
|
|
||||||
int start_x, int start_y, int size_x, int size_y,
|
|
||||||
int rotate, int flags);
|
|
||||||
|
|
||||||
|
|
||||||
[DllImport(DllName)]
|
|
||||||
private static extern IntPtr FPDF_LoadMemDocument(byte[] data, int size, string password);
|
|
||||||
|
|
||||||
public static IntPtr LoadDocumentFromBytes(byte[] data, string password)
|
|
||||||
{
|
|
||||||
InitLibrary();
|
|
||||||
if (data == null || data.Length == 0) return IntPtr.Zero;
|
|
||||||
return FPDF_LoadMemDocument(data, data.Length, string.IsNullOrEmpty(password) ? null : password);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
// 上层直接使用的安全包装
|
|
||||||
// ------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>打开 PDF 文件。失败返回 IntPtr.Zero,可用 LastErrorText 查原因。</summary>
|
|
||||||
public static IntPtr LoadDocument(string path, string password)
|
|
||||||
{
|
|
||||||
InitLibrary();
|
|
||||||
if (string.IsNullOrEmpty(path)) return IntPtr.Zero;
|
|
||||||
return FPDF_LoadDocument(path, string.IsNullOrEmpty(password) ? null : password);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void CloseDocument(IntPtr doc)
|
|
||||||
{
|
|
||||||
if (doc != IntPtr.Zero) FPDF_CloseDocument(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int GetPageCount(IntPtr doc)
|
|
||||||
{
|
|
||||||
if (doc == IntPtr.Zero) return 0;
|
|
||||||
return FPDF_GetPageCount(doc);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>取指定页尺寸(PDF 点,1/72 英寸)。失败返回 false。</summary>
|
|
||||||
public static bool GetPageSize(IntPtr doc, int pageIndex, out float widthPt, out float heightPt)
|
|
||||||
{
|
|
||||||
widthPt = 0; heightPt = 0;
|
|
||||||
if (doc == IntPtr.Zero) return false;
|
|
||||||
double w, h;
|
|
||||||
bool ok = FPDF_GetPageSizeByIndex(doc, pageIndex, out w, out h) != 0;
|
|
||||||
widthPt = (float)w;
|
|
||||||
heightPt = (float)h;
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IntPtr LoadPage(IntPtr doc, int pageIndex)
|
|
||||||
{
|
|
||||||
if (doc == IntPtr.Zero) return IntPtr.Zero;
|
|
||||||
return FPDF_LoadPage(doc, pageIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void ClosePage(IntPtr page)
|
|
||||||
{
|
|
||||||
if (page != IntPtr.Zero) FPDF_ClosePage(page);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>把整页渲染到 GDI+ 位图(白底不透明)。</summary>
|
|
||||||
public static System.Drawing.Bitmap RenderPageToBitmap(IntPtr page, int widthPx, int heightPx)
|
|
||||||
{
|
|
||||||
return RenderRegionToBitmap(page, 0, 0, widthPx, heightPx, widthPx, heightPx);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 把页面局部区域渲染到位图(白底不透明)。
|
|
||||||
/// 页面整图尺寸 = fullW×fullH 像素;只取 (startX,startY,wPx,hPx) 矩形。
|
|
||||||
/// 用于:OCR 框选区域按任意分辨率高清重渲染(与显示缩放无关)。
|
|
||||||
/// </summary>
|
|
||||||
public static System.Drawing.Bitmap RenderRegionToBitmap(IntPtr page,
|
|
||||||
int startX, int startY, int wPx, int hPx, int fullW, int fullH)
|
|
||||||
{
|
|
||||||
if (page == IntPtr.Zero || wPx <= 0 || hPx <= 0) return null;
|
|
||||||
if (wPx > 16384 || hPx > 16384) return null;
|
|
||||||
|
|
||||||
IntPtr bmp = FPDFBitmap_Create(wPx, hPx, 1);
|
|
||||||
if (bmp == IntPtr.Zero) return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// 白底不透明(0xAARRGGBB)
|
|
||||||
FPDFBitmap_FillRect(bmp, 0, 0, wPx, hPx, 0xFFFFFFFF);
|
|
||||||
// start_x/start_y 为负偏移:位图原点相对页面原点
|
|
||||||
FPDF_RenderPageBitmap(bmp, page, -startX, -startY, fullW, fullH, 0, FPDF_ANNOT);
|
|
||||||
|
|
||||||
IntPtr buffer = FPDFBitmap_GetBuffer(bmp);
|
|
||||||
int stride = FPDFBitmap_GetStride(bmp);
|
|
||||||
if (buffer == IntPtr.Zero || stride <= 0) return null;
|
|
||||||
|
|
||||||
// pdfium BGRA(top-down) → GDI+ Format32bppArgb(BGRA) 逐行拷贝
|
|
||||||
var result = new System.Drawing.Bitmap(wPx, hPx,
|
|
||||||
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
var rect = new System.Drawing.Rectangle(0, 0, wPx, hPx);
|
|
||||||
var data = result.LockBits(rect,
|
|
||||||
System.Drawing.Imaging.ImageLockMode.WriteOnly,
|
|
||||||
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for (int y = 0; y < hPx; y++)
|
|
||||||
{
|
|
||||||
IntPtr src = new IntPtr(buffer.ToInt64() + (long)y * stride);
|
|
||||||
IntPtr dst = new IntPtr(data.Scan0.ToInt64() + (long)y * data.Stride);
|
|
||||||
NativeMethods.CopyMemory(dst, src, (uint)(wPx * 4));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
result.UnlockBits(data);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
FPDFBitmap_Destroy(bmp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string LastErrorText()
|
|
||||||
{
|
|
||||||
uint err = FPDF_GetLastError();
|
|
||||||
switch (err)
|
|
||||||
{
|
|
||||||
case FPDF_ERR_SUCCESS: return "成功";
|
|
||||||
case FPDF_ERR_FILE: return "文件无法打开或读取";
|
|
||||||
case FPDF_ERR_FORMAT: return "文件不是有效 PDF 或已损坏";
|
|
||||||
case FPDF_ERR_PASSWORD: return "PDF 已加密,需要密码";
|
|
||||||
case FPDF_ERR_SECURITY: return "不支持的安全方案";
|
|
||||||
case FPDF_ERR_PAGE: return "页面内容无法解析";
|
|
||||||
default: return "未知错误(" + err + ")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class NativeMethods
|
|
||||||
{
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
|
|
||||||
public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// 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 pdfbt
|
|
||||||
{
|
|
||||||
/// <summary>百度千帆 PaddleOCR-VL 客户端(单实例)。</summary>
|
|
||||||
internal sealed class QianfanOcrClient
|
|
||||||
{
|
|
||||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
|
||||||
private string _apiKey;
|
|
||||||
private const string BaseUrl = "https://qianfan.baidubce.com/v2/ocr/paddleocr";
|
|
||||||
|
|
||||||
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
|
|
||||||
public event Action<string> StatusChanged;
|
|
||||||
|
|
||||||
public QianfanOcrClient()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ServicePointManager.SecurityProtocol =
|
|
||||||
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsConfigured
|
|
||||||
{
|
|
||||||
get { return !string.IsNullOrWhiteSpace(_apiKey); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 qianfan_api_key 注入(热更新)。</summary>
|
|
||||||
public void SetApiKey(string key)
|
|
||||||
{
|
|
||||||
_apiKey = key == null ? "" : key.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</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("千帆 PaddleOCR-VL 未配置 API Key(OCR 设置窗口里填千帆 API Key)");
|
|
||||||
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
|
|
||||||
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<OcrItem> 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<string, object>
|
|
||||||
{
|
|
||||||
{ "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<OcrItem> ParseResponse(string respText, int cropW, int cropH)
|
|
||||||
{
|
|
||||||
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
|
|
||||||
if (msg == null) throw new Exception("千帆响应解析失败(空响应)");
|
|
||||||
if (msg.ContainsKey("error"))
|
|
||||||
{
|
|
||||||
var err = msg["error"] as Dictionary<string, object>;
|
|
||||||
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<OcrItem>();
|
|
||||||
// result.layoutParsingResults[0].prunedResult.parsing_res_list
|
|
||||||
var result = msg.ContainsKey("result") ? msg["result"] as Dictionary<string, object> : 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<string, object>;
|
|
||||||
var pruned = first != null && first.ContainsKey("prunedResult")
|
|
||||||
? first["prunedResult"] as Dictionary<string, object> : 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<string, object> 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("<tr[^>]*>(.*?)</tr>",
|
|
||||||
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("<t[dh][^>]*>(.*?)</t[dh]>",
|
|
||||||
System.Text.RegularExpressions.RegexOptions.Singleline |
|
|
||||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
|
|
||||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
|
||||||
|
|
||||||
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
|
|
||||||
private static void AddTableItems(List<OcrItem> items, string tableHtml,
|
|
||||||
float x1, float y1, float x2, float y2)
|
|
||||||
{
|
|
||||||
var rows = new List<string>();
|
|
||||||
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
|
|
||||||
{
|
|
||||||
var cells = new List<string>();
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -10,7 +10,7 @@ using System.Drawing.Imaging;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using 成套报价软件; // ★ A1:OCR/PDF 基础设施已链接共享主项目源码(类型在 成套报价软件 命名空间)
|
||||||
namespace pdfbt
|
namespace pdfbt
|
||||||
{
|
{
|
||||||
internal static class SelfTest
|
internal static class SelfTest
|
||||||
|
|||||||
@@ -1,221 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// UmiOcrClient:Umi-OCR 本地 OCR 引擎(hiroi-sora/Umi-OCR,离线免费)
|
|
||||||
// · 前提:用户在本机运行 Umi-OCR 主程序,全局设置里开启 HTTP 服务(默认开启,端口 1224)
|
|
||||||
// · POST {BaseUrl}/api/ocr application/json:
|
|
||||||
// { "base64": "<纯base64无前缀>", "options": { "data.format": "dict" } }
|
|
||||||
// · 响应: code=100 成功;data=[{ text, score, box:[[x,y]×4 左上/右上/右下/左下], end }]
|
|
||||||
// code=101 无文本(data 为原因字符串);其余 code 失败
|
|
||||||
// · 地址存 baidu_ocr.json 的 umi_url(OCR设置窗口写,默认 http://127.0.0.1:1224)
|
|
||||||
// · 本地引擎无需 API Key,IsConfigured 恒 true
|
|
||||||
// ============================================================
|
|
||||||
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>Umi-OCR 本地引擎客户端(单实例)。</summary>
|
|
||||||
internal sealed class UmiOcrClient
|
|
||||||
{
|
|
||||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
|
||||||
private string _baseUrl = "http://127.0.0.1:1224";
|
|
||||||
|
|
||||||
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
|
|
||||||
public event Action<string> StatusChanged;
|
|
||||||
|
|
||||||
public bool IsConfigured
|
|
||||||
{
|
|
||||||
get { return true; } // 本地引擎无需 Key
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>服务地址由 OcrClient 从 baidu_ocr.json 的 umi_url 注入(热更新)。</summary>
|
|
||||||
public void SetBaseUrl(string url)
|
|
||||||
{
|
|
||||||
url = (url == null ? "" : url.Trim());
|
|
||||||
if (url.Length == 0) url = "http://127.0.0.1:1224";
|
|
||||||
if (!url.StartsWith("http")) url = "http://" + url;
|
|
||||||
_baseUrl = url.TrimEnd('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>识别一块位图 → 文本行列表(带位置)。JSON POST 到本机 Umi-OCR。</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>());
|
|
||||||
|
|
||||||
return Task.Factory.StartNew(() =>
|
|
||||||
{
|
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
|
||||||
var result = RequestAndParse(crop, timeoutMs);
|
|
||||||
Notify("Umi-OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
|
|
||||||
{
|
|
||||||
// PNG base64(无 data: 前缀);PNG 保坐标精度,本地服务无 4MB 限制
|
|
||||||
string b64;
|
|
||||||
using (var ms = new MemoryStream())
|
|
||||||
{
|
|
||||||
crop.Save(ms, ImageFormat.Png);
|
|
||||||
b64 = Convert.ToBase64String(ms.ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
string req = _json.Serialize(new Dictionary<string, object>
|
|
||||||
{
|
|
||||||
{ "base64", b64 },
|
|
||||||
{ "options", new Dictionary<string, object> { { "data.format", "dict" } } }
|
|
||||||
});
|
|
||||||
|
|
||||||
string body;
|
|
||||||
using (var wc = new WebClient())
|
|
||||||
{
|
|
||||||
wc.Encoding = Encoding.UTF8;
|
|
||||||
wc.Headers["Content-Type"] = "application/json";
|
|
||||||
wc.Proxy = null; // 本地地址不走系统代理(配了代理会把 127.0.0.1 也代理走)
|
|
||||||
body = wc.UploadString(_baseUrl + "/api/ocr", "POST", req);
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp = _json.Deserialize<Dictionary<string, object>>(body);
|
|
||||||
if (resp == null || !resp.ContainsKey("code"))
|
|
||||||
throw new Exception("Umi-OCR 返回解析失败");
|
|
||||||
|
|
||||||
int code;
|
|
||||||
int.TryParse(Convert.ToString(resp["code"]), out code);
|
|
||||||
if (code == 101)
|
|
||||||
return new List<OcrItem>(); // 无文本,不算错误
|
|
||||||
if (code != 100)
|
|
||||||
throw new Exception("Umi-OCR 错误 code=" + code + ": " +
|
|
||||||
Convert.ToString(resp.ContainsKey("data") ? resp["data"] : ""));
|
|
||||||
|
|
||||||
var items = new List<OcrItem>();
|
|
||||||
var arr = resp.ContainsKey("data") ? resp["data"] as System.Collections.ArrayList : null;
|
|
||||||
if (arr != null)
|
|
||||||
{
|
|
||||||
foreach (Dictionary<string, object> w in arr)
|
|
||||||
{
|
|
||||||
if (w == null) continue;
|
|
||||||
var boxArr = w.ContainsKey("box") ? w["box"] as System.Collections.ArrayList : null;
|
|
||||||
if (boxArr == null || boxArr.Count < 4) continue;
|
|
||||||
var pts = new PointF[4];
|
|
||||||
for (int i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
var xy = boxArr[i] as System.Collections.ArrayList;
|
|
||||||
float x, y;
|
|
||||||
float.TryParse(Convert.ToString(xy != null && xy.Count > 0 ? xy[0] : "0"), out x);
|
|
||||||
float.TryParse(Convert.ToString(xy != null && xy.Count > 1 ? xy[1] : "0"), out y);
|
|
||||||
pts[i] = new PointF(x, y);
|
|
||||||
}
|
|
||||||
float score = 0.95f;
|
|
||||||
if (w.ContainsKey("score"))
|
|
||||||
{
|
|
||||||
float sv;
|
|
||||||
if (float.TryParse(Convert.ToString(w["score"]),
|
|
||||||
System.Globalization.NumberStyles.Float,
|
|
||||||
System.Globalization.CultureInfo.InvariantCulture, out sv)) score = sv;
|
|
||||||
}
|
|
||||||
items.Add(new OcrItem
|
|
||||||
{
|
|
||||||
Text = Convert.ToString(w.ContainsKey("text") ? w["text"] : ""),
|
|
||||||
Box = pts, // box 即发送图像素坐标(顺时针四点),与 OcrItem 语义一致
|
|
||||||
Score = score
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Notify(string msg)
|
|
||||||
{
|
|
||||||
var h = StatusChanged;
|
|
||||||
if (h != null) h(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
// ============================================================
|
|
||||||
// UmiOcrHost:Umi-OCR 本体(网盘 zip)下载/安装/启停/检测 管理器
|
|
||||||
// · 本体 zip 直链(用户网盘,~179MB,zip 根直接是 Umi-OCR.exe 无顶层目录)
|
|
||||||
// · 安装 = 下载 zip 到临时文件 → 解压到 exe 旁 Umi-OCR\ → 找到主程序验证 → 删 zip
|
|
||||||
// · 启动 = Process.Start(主程序,Umi-OCR 启动即带 HTTP 服务 1224 端口) → 轮询端口就绪
|
|
||||||
// · 停止 = 只停由本程序启动的实例(CloseMainWindow→超时 Kill),不碰用户自己开的
|
|
||||||
// ============================================================
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
|
|
||||||
namespace pdfbt
|
|
||||||
{
|
|
||||||
internal static class UmiOcrHost
|
|
||||||
{
|
|
||||||
/// <summary>本体 zip 直链(用户网盘直连,GET 才有内容,HEAD 会 404,不支持断点续传)。</summary>
|
|
||||||
public const string DownloadUrl = "http://s333.odn.cc/s/845f64a239b665ec";
|
|
||||||
|
|
||||||
private const string InstallDirName = "Umi-OCR";
|
|
||||||
private const int HttpPort = 1224;
|
|
||||||
|
|
||||||
/// <summary>由本程序启动的实例(停止时只关它,不碰用户自己开的)。</summary>
|
|
||||||
private static Process _startedProc;
|
|
||||||
|
|
||||||
/// <summary>本体安装目录(exe 旁 Umi-OCR\)。</summary>
|
|
||||||
public static string InstallDir
|
|
||||||
{
|
|
||||||
get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, InstallDirName); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>找安装目录里的主程序(Umi-OCR.exe,大小写不敏感;找不到返回 null)。</summary>
|
|
||||||
public static string FindExe()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(InstallDir)) return null;
|
|
||||||
foreach (var f in Directory.GetFiles(InstallDir, "*.exe", SearchOption.TopDirectoryOnly))
|
|
||||||
{
|
|
||||||
string name = Path.GetFileName(f);
|
|
||||||
if (name.Equals("Umi-OCR.exe", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
name.Equals("Umi-OCR.exe", StringComparison.OrdinalIgnoreCase))
|
|
||||||
return f;
|
|
||||||
}
|
|
||||||
// 目录名不完全一致时兜底:取第一个 exe
|
|
||||||
string[] all = Directory.GetFiles(InstallDir, "*.exe", SearchOption.TopDirectoryOnly);
|
|
||||||
return all.Length > 0 ? all[0] : null;
|
|
||||||
}
|
|
||||||
catch { return null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>本体是否已安装(目录里能找到主程序)。</summary>
|
|
||||||
public static bool IsInstalled
|
|
||||||
{
|
|
||||||
get { return FindExe() != null; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>HTTP 服务(1224 端口)是否在监听。</summary>
|
|
||||||
public static bool IsHttpUp()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var c = new System.Net.Sockets.TcpClient())
|
|
||||||
{
|
|
||||||
var ar = c.BeginConnect("127.0.0.1", HttpPort, null, null);
|
|
||||||
if (ar.AsyncWaitHandle.WaitOne(800))
|
|
||||||
{
|
|
||||||
c.EndConnect(ar);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
c.Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>三态:0=未安装,1=已安装未运行,2=运行中(HTTP 在线)。</summary>
|
|
||||||
public static int Detect()
|
|
||||||
{
|
|
||||||
if (!IsInstalled) return 0;
|
|
||||||
return IsHttpUp() ? 2 : 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>状态文字(设置窗口显示用)。</summary>
|
|
||||||
public static string DetectText()
|
|
||||||
{
|
|
||||||
int st = Detect();
|
|
||||||
if (st == 0) return "未检测到 OCR 本体(点\"下载 OCR 本体\"自动下载安装)";
|
|
||||||
if (st == 1) return "已检测到 OCR 本体,服务未运行(识别时会自动启动)";
|
|
||||||
return "OCR 本体运行中(HTTP " + HttpPort + " 端口在线)";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>下载 zip → 解压到 Umi-OCR\ → 验证主程序 → 删 zip。
|
|
||||||
/// progress: 0~100 下载百分比;解压与收尾阶段回调 100。失败抛异常(内部已清理残留)。
|
|
||||||
/// ★ 必须用 DownloadFileAsync:同步版 DownloadFile 不触发 DownloadProgressChanged
|
|
||||||
/// (进度事件只在异步路径发,同步下载会一直停在 0%)。</summary>
|
|
||||||
public static Task DownloadAndInstallAsync(Action<int> progress)
|
|
||||||
{
|
|
||||||
return Task.Factory.StartNew(delegate
|
|
||||||
{
|
|
||||||
string tmpZip = Path.Combine(Path.GetTempPath(), "Umi-OCR_" + Guid.NewGuid().ToString("N") + ".zip");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (progress != null) progress(0);
|
|
||||||
// 异步下载包成同步等待(TaskCompletionSource),进度事件才会触发
|
|
||||||
var dlTcs = new TaskCompletionSource<bool>();
|
|
||||||
using (var wc = new WebClient())
|
|
||||||
{
|
|
||||||
wc.Proxy = null;
|
|
||||||
wc.DownloadProgressChanged += (s, e) =>
|
|
||||||
{
|
|
||||||
if (progress != null) progress(e.ProgressPercentage);
|
|
||||||
};
|
|
||||||
wc.DownloadFileCompleted += (s, e) =>
|
|
||||||
{
|
|
||||||
if (e.Error != null) dlTcs.TrySetException(e.Error);
|
|
||||||
else if (e.Cancelled) dlTcs.TrySetException(new Exception("下载已取消"));
|
|
||||||
else dlTcs.TrySetResult(true);
|
|
||||||
};
|
|
||||||
wc.DownloadFileAsync(new Uri(DownloadUrl), tmpZip);
|
|
||||||
}
|
|
||||||
dlTcs.Task.GetAwaiter().GetResult(); // 等下载结束(异常原样抛出)
|
|
||||||
if (progress != null) progress(100);
|
|
||||||
|
|
||||||
// 校验是 zip(PK 头)而不是错误页/JSON
|
|
||||||
using (var fs = File.OpenRead(tmpZip))
|
|
||||||
{
|
|
||||||
int b0 = fs.ReadByte(), b1 = fs.ReadByte();
|
|
||||||
if (b0 != 'P' || b1 != 'K')
|
|
||||||
throw new Exception("下载的内容不是有效的压缩包(可能是网盘临时故障),请稍后重试");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解压(覆盖);zip 根直接是 Umi-OCR.exe,目标目录即安装目录
|
|
||||||
string dst = InstallDir;
|
|
||||||
if (!Directory.Exists(dst)) Directory.CreateDirectory(dst);
|
|
||||||
using (var archive = ZipFile.OpenRead(tmpZip))
|
|
||||||
{
|
|
||||||
foreach (var entry in archive.Entries)
|
|
||||||
{
|
|
||||||
string target = Path.GetFullPath(Path.Combine(dst, entry.FullName));
|
|
||||||
if (!target.StartsWith(Path.GetFullPath(dst), StringComparison.OrdinalIgnoreCase))
|
|
||||||
continue; // 防 zip 路径穿越
|
|
||||||
if (string.IsNullOrEmpty(entry.Name))
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(target)) Directory.CreateDirectory(target);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
string tdir = Path.GetDirectoryName(target);
|
|
||||||
if (!string.IsNullOrEmpty(tdir) && !Directory.Exists(tdir)) Directory.CreateDirectory(tdir);
|
|
||||||
if (File.Exists(target))
|
|
||||||
{
|
|
||||||
try { File.Delete(target); } catch { } // 被占用则跳过(旧文件同名多半一致)
|
|
||||||
}
|
|
||||||
entry.ExtractToFile(target, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FindExe() == null)
|
|
||||||
throw new Exception("解压完成但未找到 Umi-OCR 主程序,压缩包内容可能不完整");
|
|
||||||
|
|
||||||
try { File.Delete(tmpZip); } catch { } // 验证通过后删 zip
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
try { if (File.Exists(tmpZip)) File.Delete(tmpZip); } catch { }
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>确保本体已安装且 HTTP 服务在线:未安装抛错引导下载;未运行则静默启动(--hide 主窗口进托盘)并等端口就绪(≤15秒)。
|
|
||||||
/// 返回 false=已安装但启动超时(调用方提示)。</summary>
|
|
||||||
public static bool EnsureRunning()
|
|
||||||
{
|
|
||||||
if (!IsInstalled)
|
|
||||||
throw new Exception("尚未安装 Umi-OCR 本体。\r\n请到 OCR 设置窗口选择 Umi-OCR 引擎,点\"下载 OCR 本体\"。");
|
|
||||||
if (IsHttpUp()) return true;
|
|
||||||
|
|
||||||
string exe = FindExe();
|
|
||||||
// --hide:官方参数,启动后主窗口隐藏到托盘,后台提供 HTTP 服务(用户看到零弹窗)
|
|
||||||
_startedProc = Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = exe,
|
|
||||||
Arguments = "--hide",
|
|
||||||
WorkingDirectory = Path.GetDirectoryName(exe),
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden
|
|
||||||
});
|
|
||||||
for (int i = 0; i < 30; i++) // 30 × 500ms = 最多 15 秒
|
|
||||||
{
|
|
||||||
if (IsHttpUp()) return true;
|
|
||||||
System.Threading.Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>停止由本程序启动的实例(PdfViewerWindow 关闭时调用;用户自己开的不动)。
|
|
||||||
/// --hide 启动的窗口在托盘,CloseMainWindow 不可靠:先发官方 --quit 指令优雅退出,超时再 Kill。</summary>
|
|
||||||
public static void StopIfStartedByUs()
|
|
||||||
{
|
|
||||||
var p = _startedProc;
|
|
||||||
_startedProc = null;
|
|
||||||
if (p == null) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!p.HasExited)
|
|
||||||
{
|
|
||||||
// 官方退出指令:向已运行实例发 --quit(依赖其 HTTP 通道)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Process.Start(new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = FindExe(),
|
|
||||||
Arguments = "--quit",
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (!p.WaitForExit(3000)) p.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
finally { try { p.Dispose(); } catch { } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+39
-14
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ProjectGuid>{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}</ProjectGuid>
|
<ProjectGuid>{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}</ProjectGuid>
|
||||||
@@ -113,16 +113,43 @@
|
|||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="SelfTest.cs" />
|
<Compile Include="SelfTest.cs" />
|
||||||
<Compile Include="PdfBridge.cs" />
|
<Compile Include="PdfBridge.cs" />
|
||||||
<Compile Include="PdfiumNative.cs" />
|
<!-- ★ A1(2026-10-01):OCR/PDF 基础设施与主项目共享同一份源码(链接编译,不再各留副本)——
|
||||||
<Compile Include="PdfDocument.cs" />
|
权威源 成套报价软件\扒图\(主项目版为演进版,含 OcrPaths 旧布局回退/ocr.cls 方向纠正/
|
||||||
<Compile Include="OcrClient.cs" />
|
UmiOcr 启停拆分等修复);此前 pdfbt 侧 10 个本地副本是旧快照,已删除(git 基线可找回) -->
|
||||||
<Compile Include="GlmOcrClient.cs" />
|
<Compile Include="..\成套报价软件\扒图\PdfiumNative.cs">
|
||||||
<Compile Include="QianfanOcrClient.cs" />
|
<Link>共享扒图\PdfiumNative.cs</Link>
|
||||||
<Compile Include="UapisOcrClient.cs" />
|
</Compile>
|
||||||
<Compile Include="UmiOcrClient.cs" />
|
<Compile Include="..\成套报价软件\扒图\PdfDocument.cs">
|
||||||
<Compile Include="UmiOcrHost.cs" />
|
<Link>共享扒图\PdfDocument.cs</Link>
|
||||||
<Compile Include="OcrSettingsForm.cs" />
|
</Compile>
|
||||||
<Compile Include="OcrSettingsForm.Designer.cs" />
|
<Compile Include="..\成套报价软件\扒图\OcrClient.cs">
|
||||||
|
<Link>共享扒图\OcrClient.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\GlmOcrClient.cs">
|
||||||
|
<Link>共享扒图\GlmOcrClient.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\QianfanOcrClient.cs">
|
||||||
|
<Link>共享扒图\QianfanOcrClient.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\UapisOcrClient.cs">
|
||||||
|
<Link>共享扒图\UapisOcrClient.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\UmiOcrClient.cs">
|
||||||
|
<Link>共享扒图\UmiOcrClient.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\UmiOcrHost.cs">
|
||||||
|
<Link>共享扒图\UmiOcrHost.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\UmiOcrLauncher.cs">
|
||||||
|
<Link>共享扒图\UmiOcrLauncher.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\OcrSettingsForm.cs">
|
||||||
|
<Link>共享扒图\OcrSettingsForm.cs</Link>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="..\成套报价软件\扒图\OcrSettingsForm.Designer.cs">
|
||||||
|
<Link>共享扒图\OcrSettingsForm.Designer.cs</Link>
|
||||||
|
<DependentUpon>OcrSettingsForm.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
<Compile Include="PdfViewerWindow.cs" />
|
<Compile Include="PdfViewerWindow.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Plugin\Form3.cs" />
|
<Compile Include="Plugin\Form3.cs" />
|
||||||
@@ -155,9 +182,7 @@
|
|||||||
<EmbeddedResource Include="MainForm.resx">
|
<EmbeddedResource Include="MainForm.resx">
|
||||||
<DependentUpon>MainForm.cs</DependentUpon>
|
<DependentUpon>MainForm.cs</DependentUpon>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Include="OcrSettingsForm.resx">
|
<!-- OcrSettingsForm.resx 删除:表单已链接共享主项目源码(A1),本地空壳 resx 与链接 Designer 不再匹配 -->
|
||||||
<DependentUpon>OcrSettingsForm.Designer.cs</DependentUpon>
|
|
||||||
</EmbeddedResource>
|
|
||||||
<EmbeddedResource Include="Plugin\Form3.resx">
|
<EmbeddedResource Include="Plugin\Form3.resx">
|
||||||
<DependentUpon>Form3.cs</DependentUpon>
|
<DependentUpon>Form3.cs</DependentUpon>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ shezhi\许可证文本.Designer.cs
|
|||||||
登录.Designer.cs
|
登录.Designer.cs
|
||||||
会员服务Host.cs
|
会员服务Host.cs
|
||||||
OpenClawHost.cs
|
OpenClawHost.cs
|
||||||
|
数据库连接.cs
|
||||||
AI助手接口.cs
|
AI助手接口.cs
|
||||||
AI助手设置.cs
|
AI助手设置.cs
|
||||||
AI助手设置.Designer.cs
|
AI助手设置.Designer.cs
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ namespace 成套报价软件
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_projectId = projectId;
|
_projectId = projectId;
|
||||||
_dbPath = dbPath;
|
_dbPath = dbPath;
|
||||||
_connString = "Data Source=" + dbPath + ";Version=3;";
|
// ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串
|
||||||
|
_connString = 数据库连接.串(dbPath);
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
this.button1.Click += button1_Click;
|
this.button1.Click += button1_Click;
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ namespace 成套报价软件
|
|||||||
|
|
||||||
private string GetConnString(string dbPath)
|
private string GetConnString(string dbPath)
|
||||||
{
|
{
|
||||||
return "Data Source=" + dbPath + ";Version=3;";
|
// ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串
|
||||||
|
return 数据库连接.串(dbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, string> LoadProjectInfo()
|
private Dictionary<string, string> LoadProjectInfo()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -503,7 +503,8 @@ namespace 成套报价软件
|
|||||||
|
|
||||||
private string GetConnString(string dbPath)
|
private string GetConnString(string dbPath)
|
||||||
{
|
{
|
||||||
return "Data Source=" + dbPath + ";Version=3;";
|
// ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串
|
||||||
|
return 数据库连接.串(dbPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>查表索引(复用连接版本)。</summary>
|
/// <summary>查表索引(复用连接版本)。</summary>
|
||||||
|
|||||||
+2
-3
@@ -3663,9 +3663,8 @@ namespace 成套报价软件
|
|||||||
|
|
||||||
private string GetConnString(string dbPath)
|
private string GetConnString(string dbPath)
|
||||||
{
|
{
|
||||||
// ★ B4(2026-10-01):加 BusyTimeout=5000——直写队列/全库重算/扒图窗口并发访问同一库时,
|
// ★ A2(2026-10-01):统一收敛到 数据库连接.串(一处管理),本方法保留签名做转发,调用方零改动
|
||||||
// 遇锁默认 0 等待直接抛 database is locked;加超时后 SQLite 自动等待重试(方案窗口早已同款)
|
return 数据库连接.串(dbPath);
|
||||||
return "Data Source=" + dbPath + ";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -106,11 +106,11 @@ namespace 成套报价软件
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>GetConnString 的静态包装,供静态方法调用。</summary>
|
/// <summary>GetConnString 的静态包装,供静态方法调用。
|
||||||
|
/// ★ A2(2026-10-01):统一收敛到 数据库连接.串(一处管理),本方法保留签名做转发。</summary>
|
||||||
private static string GetConnStringStatic(string dbPath)
|
private static string GetConnStringStatic(string dbPath)
|
||||||
{
|
{
|
||||||
// ★ B4(2026-10-01):与实例版同款加 BusyTimeout(防并发锁直报错)
|
return 数据库连接.串(dbPath);
|
||||||
return "Data Source=\"" + dbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
|
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ProjectGuid>{B6966FA3-E161-44ED-8A40-CCDBE1F15192}</ProjectGuid>
|
<ProjectGuid>{B6966FA3-E161-44ED-8A40-CCDBE1F15192}</ProjectGuid>
|
||||||
@@ -159,6 +159,7 @@
|
|||||||
<Compile Include="DockWindows\DockWindow10\DockWindow10.cs" />
|
<Compile Include="DockWindows\DockWindow10\DockWindow10.cs" />
|
||||||
<Compile Include="DockWindows\DockWindow10\DockWindow10.Designer.cs" />
|
<Compile Include="DockWindows\DockWindow10\DockWindow10.Designer.cs" />
|
||||||
<Compile Include="OpenClawHost.cs" />
|
<Compile Include="OpenClawHost.cs" />
|
||||||
|
<Compile Include="数据库连接.cs" />
|
||||||
<Compile Include="AI助手接口.cs" />
|
<Compile Include="AI助手接口.cs" />
|
||||||
<Compile Include="AI助手设置.cs" />
|
<Compile Include="AI助手设置.cs" />
|
||||||
<Compile Include="AI助手设置.Designer.cs" />
|
<Compile Include="AI助手设置.Designer.cs" />
|
||||||
|
|||||||
@@ -612,9 +612,8 @@ namespace MyApp
|
|||||||
|
|
||||||
private string GetConnString(string dbPath)
|
private string GetConnString(string dbPath)
|
||||||
{
|
{
|
||||||
// ★ busy_timeout=3000:数据库被 CAD 或其他进程短暂占用时等待重试而非卡死;
|
// ★ A2(2026-10-01):统一收敛到主程序 数据库连接.串无池(保留扒图关池语义,一处管理)
|
||||||
// Pooling 关闭,避免连接池长期持有文件句柄
|
return 成套报价软件.数据库连接.串无池(dbPath);
|
||||||
return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetQuoteDbPath(string projectId)
|
private string GetQuoteDbPath(string projectId)
|
||||||
|
|||||||
@@ -635,9 +635,8 @@ namespace MyAppPdf
|
|||||||
|
|
||||||
private string GetConnString(string dbPath)
|
private string GetConnString(string dbPath)
|
||||||
{
|
{
|
||||||
// ★ busy_timeout=3000:数据库被 CAD 或其他进程短暂占用时等待重试而非卡死;
|
// ★ A2(2026-10-01):统一收敛到主程序 数据库连接.串无池(保留扒图关池语义,一处管理)
|
||||||
// Pooling 关闭,避免连接池长期持有文件句柄
|
return 成套报价软件.数据库连接.串无池(dbPath);
|
||||||
return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetQuoteDbPath(string projectId)
|
private string GetQuoteDbPath(string projectId)
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace 成套报价软件
|
||||||
|
{
|
||||||
|
/// <summary>SQLite 连接字符串统一管理(A2,2026-10-01):全项目连接参数一处调整全局生效。
|
||||||
|
/// 此前 6 份重复定义(主窗口/菜单与库/调系数/甲方报表/Cad扒图/Pdf扒图)参数漂移——
|
||||||
|
/// 调系数与甲方报表是裸串(无 WAL/超时,并发易锁死),扒图窗口是有意关池(防句柄常驻)。
|
||||||
|
/// 现收敛为两个变体:各处私有 GetConnString 均转发到这里,调用方零改动。</summary>
|
||||||
|
public static class 数据库连接
|
||||||
|
{
|
||||||
|
/// <summary>标准连接串(主窗口/直写队列/重算/报表等主程序各模块用):
|
||||||
|
/// WAL+Normal 同步+连接池+5秒锁等待。项目库文件首开后即持久保持 WAL 模式,
|
||||||
|
/// 各连接串里是否声明 Journal Mode 只影响新库,对已建库无实际差异。</summary>
|
||||||
|
public static string 串(string dbPath)
|
||||||
|
{
|
||||||
|
// ★ BusyTimeout=5000(B4):直写队列/全库重算/扒图窗口并发访问同一库时,
|
||||||
|
// 遇锁默认 0 等待直接抛 database is locked;加超时后 SQLite 自动等待重试
|
||||||
|
return "Data Source=\"" + dbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>无池连接串(扒图窗口 Cad/Pdf Form3 专用):
|
||||||
|
/// ★ Pooling 关闭是该窗口的历史有意设计——避免连接池长期持有库文件句柄
|
||||||
|
/// (导出项目复制库/删库等文件操作不受池句柄干扰),收敛时保留该语义;
|
||||||
|
/// busy_timeout=3000:库被其他模块短暂占用时等待重试而非卡死。</summary>
|
||||||
|
public static string 串无池(string dbPath)
|
||||||
|
{
|
||||||
|
return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user