diff --git a/pdfbt/GlmOcrClient.cs b/pdfbt/GlmOcrClient.cs deleted file mode 100644 index ec701da..0000000 --- a/pdfbt/GlmOcrClient.cs +++ /dev/null @@ -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 -{ - /// 智谱 GLM-OCR 客户端(单实例)。 - internal sealed class GlmOcrClient - { - private readonly JavaScriptSerializer _json = new JavaScriptSerializer(); - private string _apiKey; - private string _baseUrl; - - /// 状态提示(未配置Key/请求失败等),接 OcrClient.StatusChanged。 - public event Action 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 { } - } - - /// Key 由 OcrClient 从 baidu_ocr.json 的 glm_api_key 注入(热更新)。 - 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; } - } - - /// 识别一块位图 → 文本块列表(带 bbox 像素坐标)。 - public Task> RecognizeAsync(Bitmap crop, int timeoutMs) - { - if (crop == null || crop.Width <= 0 || crop.Height <= 0) - return Task.FromResult(new List()); - - if (!IsConfigured) - { - Notify("GLM OCR 未配置 API Key(OCR 设置窗口里填智谱 API Key)"); - var tcs0 = new TaskCompletionSource>(); - 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 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 - { - { "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); - } - - /// 解析 layout_parsing 响应 → OcrItem 列表(text 块)。 - private List ParseResponse(string respText, int cropW, int cropH) - { - var msg = _json.Deserialize>(respText); - if (msg == null) throw new Exception("GLM 响应解析失败(空响应)"); - if (msg.ContainsKey("error")) - { - var err = msg["error"] as Dictionary; - 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(); - 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 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 为 HTML): - // 每个 一条文本(单元格空格拼接),行高均分定位。 - // 必须在 CleanHtml 剥标签之前解析结构。 - AddTableItems(items, content, x1, y1, x2, y2); - continue; - } - if (label != "text") continue; // 图片/公式块跳过 - content = CleanHtmlShared(content); // 居中块会返回
..
等 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("]*>(.*?)", - 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("]*>(.*?)", - System.Text.RegularExpressions.RegexOptions.Singleline | - System.Text.RegularExpressions.RegexOptions.IgnoreCase | - System.Text.RegularExpressions.RegexOptions.Compiled); - - /// 表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。 - private static void AddTableItems(List items, string tableHtml, - float x1, float y1, float x2, float y2) - { - var rows = new List(); - foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml)) - { - var cells = new List(); - 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); - - /// - /// 剥离 GLM content 里的 HTML 片段(居中块返回 div 等)与常见实体; - /// 标签替换为空格,避免前后文字误粘连;连续空格合并。 - /// - 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; - } - - /// - /// 清理 GLM 返回 content 里的 Markdown 标记(★保守规则,不伤正文): - /// · 行首 "##"/"#" 标题井号(仅行首;"2#配电箱" 中间的 # 保留) - /// · 成对 **加粗** 星号、行首 "- "/"* " 列表标记 - /// - 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); - } - } -} diff --git a/pdfbt/MainForm.cs b/pdfbt/MainForm.cs index cc7c47a..762c334 100644 --- a/pdfbt/MainForm.cs +++ b/pdfbt/MainForm.cs @@ -5,7 +5,7 @@ using System; using System.IO; using System.Windows.Forms; - +using 成套报价软件; // ★ A1:OCR/PDF 基础设施已链接共享主项目源码(类型在 成套报价软件 命名空间) namespace pdfbt { public partial class MainForm : Form @@ -32,7 +32,7 @@ namespace pdfbt .Deserialize>( File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json"))); 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 { } diff --git a/pdfbt/OcrClient.cs b/pdfbt/OcrClient.cs deleted file mode 100644 index 1453d07..0000000 --- a/pdfbt/OcrClient.cs +++ /dev/null @@ -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 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(); - - /// 当前 OCR 引擎(baidu/glm/qianfan/uapis/umi)。 - 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(); - } - - /// 读 baidu_ocr.json(OCR设置窗口保存后调用可热更新 Key/引擎)。 - public void LoadConfig() - { - try - { - string p = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json"); - if (File.Exists(p)) - { - var jsv = _json.Deserialize>(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); - } - - /// 获取 access_token(内存→文件缓存→在线换取)。失败抛异常。 - 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>(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>(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 - { - { "token", _token }, - { "expire", (_tokenExpire - new DateTime(1970, 1, 1)).TotalSeconds } - }; - File.WriteAllText(cachePath, _json.Serialize(cache)); - } - catch { } - return _token; - } - } - - /// 识别位图(300DPI 裁剪)。失败抛异常;成功通知引擎来源。 - /// 竖排文字(实为旋转 90° 的横排)直接识别必乱码:细高框或平均置信度偏低时, - /// 旋转 ±90° 重识别并按平均置信度选优,坐标逆向还原回原图。 - public Task> 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>(); - 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; - } - - /// 单方向识别一次(编码→上传→解析);Box 为入参图像的像素坐标。 - private List 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>(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(); - var arr = resp.ContainsKey("words_result") ? resp["words_result"] as System.Collections.ArrayList : null; - if (arr != null) - { - foreach (Dictionary w in arr) - { - var loc = (w != null && w.ContainsKey("location")) ? w["location"] as Dictionary : 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 : 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; - } - } - - /// 平均置信度(无结果时 -1,便于选优时落败)。 - private static float AvgScore(List items) - { - if (items == null || items.Count == 0) return -1f; - float sum = 0; - foreach (var it in items) sum += it.Score; - return sum / items.Count; - } - - /// 把顺/逆时针旋转 90° 后识别出的 Box 坐标逆向还原到原图坐标系 - /// (还原后重排为 左上/右上/右下/左下,Bounds 语义不变)。 - private static void UnrotateItems(List 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 loc, string key) - { - float v; - return (loc != null && loc.ContainsKey(key) && - float.TryParse(Convert.ToString(loc[key]), out v)) ? v : 0f; - } - - /// 编码为百度限额内的 JPEG(高精度版要求 base64 后 ≤4MB): - /// 质量 90/75/60 递降试编码,均超限则按 0.8 倍降采样重试(最多 8 轮,必然收敛)。 - /// scale=发送图/原图(≤1),调用方用它把返回坐标还原到原图。 - 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()); - } - } - - /// Base64 的 URI 表单转义:仅 + / = 三个字符转 %XX,字母数字无需转义。 - /// 不能用 Uri.EscapeDataString:.NET Framework 对约 32K 以上字符串抛"URI 字符串太长"。 - 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() { } - } -} diff --git a/pdfbt/OcrSettingsForm.Designer.cs b/pdfbt/OcrSettingsForm.Designer.cs deleted file mode 100644 index 3c55ff6..0000000 --- a/pdfbt/OcrSettingsForm.Designer.cs +++ /dev/null @@ -1,303 +0,0 @@ -namespace pdfbt -{ - /// OCR 设置对话框:引擎选择(百度/智谱GLM) + 对应 Key(存 exe 旁 baidu_ocr.json)。 - public partial class OcrSettingsForm : System.Windows.Forms.Form - { - /// 必需的设计器变量。 - private System.ComponentModel.IContainer components = null; - - /// 清理正在使用的资源。 - 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取消; - } -} diff --git a/pdfbt/OcrSettingsForm.cs b/pdfbt/OcrSettingsForm.cs deleted file mode 100644 index 26d595f..0000000 --- a/pdfbt/OcrSettingsForm.cs +++ /dev/null @@ -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>(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(); - } - - /// 按引擎下拉显示对应的输入区(百度=两格;智谱/千帆/UApiPro=Key一格;Umi-OCR=服务地址一格)。 - 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(); - } - - /// 刷新 Umi 本体三态状态显示。 - private void RefreshUmiStatus() - { - try { lblUmi状态.Text = UmiOcrHost.DetectText(); } catch { } - } - - /// 检测 OCR 本体:刷新状态(未安装/已安装未运行/运行中)。 - private void btnUmi检测_Click(object sender, EventArgs e) - { - RefreshUmiStatus(); - MessageBox.Show(this, UmiOcrHost.DetectText(), "检测 OCR 本体", - MessageBoxButtons.OK, MessageBoxIcon.Information); - } - - /// 下载 OCR 本体:网盘直链(~179MB)→解压到程序目录 Umi-OCR\→验证→删 zip。 - 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(); - } - - /// 帮助:全部引擎的 Key 获取方式与本地 OCR 设置流程。 - 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 - { - { "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); - } - } - } -} diff --git a/pdfbt/OcrSettingsForm.resx b/pdfbt/OcrSettingsForm.resx deleted file mode 100644 index 1af7de1..0000000 --- a/pdfbt/OcrSettingsForm.resx +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/pdfbt/PdfDocument.cs b/pdfbt/PdfDocument.cs deleted file mode 100644 index d650de0..0000000 --- a/pdfbt/PdfDocument.cs +++ /dev/null @@ -1,160 +0,0 @@ -// ============================================================ -// PdfDocument:PDF 文档封装(加载/页尺寸/按缩放渲染页位图) -// 坐标系:页面坐标 = PDF 点(1/72 英寸),原点左上(pdfium 渲染后 -// 已翻转为屏幕方向,y 向下)。 -// ============================================================ -using System; -using System.Drawing; -using System.IO; - -namespace pdfbt -{ - /// 已打开的 PDF 文档。非线程安全,只在 UI 线程使用。 - 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; } - } - - /// 打开 PDF(读入内存后走 FPDF_LoadMemDocument,免疫路径问题)。失败抛异常。 - 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; - } - } - - /// 释放文档后备缓冲(钉住句柄+字节数组)。 - 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; - } - - /// 渲染指定页。scale = 像素/PDF点。失败返回 null。 - 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); - } - - /// - /// 高清渲染页面局部区域(OCR 用):与屏幕显示缩放无关, - /// 用户任意缩放下框选,OCR 都拿到按 ocrScale 渲染的高清裁剪。 - /// 返回位图与 rectPt 一一对应(像素/pt = ocrScale)。 - /// - 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(); - } - } -} diff --git a/pdfbt/PdfViewerWindow.cs b/pdfbt/PdfViewerWindow.cs index 899fa71..4c36065 100644 --- a/pdfbt/PdfViewerWindow.cs +++ b/pdfbt/PdfViewerWindow.cs @@ -18,6 +18,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using System.Web.Script.Serialization; +using 成套报价软件; // ★ A1:OCR/PDF 基础设施已链接共享主项目源码(类型在 成套报价软件 命名空间) using System.Windows.Forms; using WeifenLuo.WinFormsUI.Docking; @@ -723,7 +724,7 @@ namespace pdfbt try { _statusLeft.Text = "正在启动 Umi-OCR 本地引擎…"; - if (!UmiOcrHost.EnsureRunning()) + if (!UmiOcrLauncher.EnsureRunning()) // ★ A1:启停已拆到 UmiOcrLauncher(主项目共享版) { _statusLeft.Text = "Umi-OCR 服务启动超时(本体已安装),请稍后重试或检查 Umi-OCR 窗口"; crop.Dispose(); @@ -1268,7 +1269,7 @@ namespace pdfbt } catch { } // Umi-OCR:只停由本程序启动的实例(用户自己开的 Umi-OCR 不动) - try { UmiOcrHost.StopIfStartedByUs(); } catch { } + try { UmiOcrLauncher.StopIfStartedByUs(); } catch { } // ★ A1:启停已拆到 UmiOcrLauncher(主项目共享版) } base.Dispose(disposing); } diff --git a/pdfbt/PdfiumNative.cs b/pdfbt/PdfiumNative.cs deleted file mode 100644 index f0430b0..0000000 --- a/pdfbt/PdfiumNative.cs +++ /dev/null @@ -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 -{ - /// pdfium C API 最小封装(仅本项目用到的函数)。 - 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(); - - /// 初始化 pdfium(进程内只需一次,线程安全)。 - 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); - } - - // ------------------------------------------------------------ - // 上层直接使用的安全包装 - // ------------------------------------------------------------ - - /// 打开 PDF 文件。失败返回 IntPtr.Zero,可用 LastErrorText 查原因。 - 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); - } - - /// 取指定页尺寸(PDF 点,1/72 英寸)。失败返回 false。 - 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); - } - - /// 把整页渲染到 GDI+ 位图(白底不透明)。 - public static System.Drawing.Bitmap RenderPageToBitmap(IntPtr page, int widthPx, int heightPx) - { - return RenderRegionToBitmap(page, 0, 0, widthPx, heightPx, widthPx, heightPx); - } - - /// - /// 把页面局部区域渲染到位图(白底不透明)。 - /// 页面整图尺寸 = fullW×fullH 像素;只取 (startX,startY,wPx,hPx) 矩形。 - /// 用于:OCR 框选区域按任意分辨率高清重渲染(与显示缩放无关)。 - /// - 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); - } - } -} diff --git a/pdfbt/QianfanOcrClient.cs b/pdfbt/QianfanOcrClient.cs deleted file mode 100644 index 174e98d..0000000 --- a/pdfbt/QianfanOcrClient.cs +++ /dev/null @@ -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 -{ - /// 百度千帆 PaddleOCR-VL 客户端(单实例)。 - internal sealed class QianfanOcrClient - { - private readonly JavaScriptSerializer _json = new JavaScriptSerializer(); - private string _apiKey; - private const string BaseUrl = "https://qianfan.baidubce.com/v2/ocr/paddleocr"; - - /// 状态提示,接 OcrClient.StatusChanged。 - public event Action StatusChanged; - - public QianfanOcrClient() - { - try - { - ServicePointManager.SecurityProtocol = - ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; - } - catch { } - } - - public bool IsConfigured - { - get { return !string.IsNullOrWhiteSpace(_apiKey); } - } - - /// Key 由 OcrClient 从 baidu_ocr.json 的 qianfan_api_key 注入(热更新)。 - public void SetApiKey(string key) - { - _apiKey = key == null ? "" : key.Trim(); - } - - /// 识别一块位图 → 文本块列表(带 bbox 像素坐标)。 - public Task> RecognizeAsync(Bitmap crop, int timeoutMs) - { - if (crop == null || crop.Width <= 0 || crop.Height <= 0) - return Task.FromResult(new List()); - - if (!IsConfigured) - { - Notify("千帆 PaddleOCR-VL 未配置 API Key(OCR 设置窗口里填千帆 API Key)"); - var tcs0 = new TaskCompletionSource>(); - 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 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 - { - { "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 ParseResponse(string respText, int cropW, int cropH) - { - var msg = _json.Deserialize>(respText); - if (msg == null) throw new Exception("千帆响应解析失败(空响应)"); - if (msg.ContainsKey("error")) - { - var err = msg["error"] as Dictionary; - 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(); - // result.layoutParsingResults[0].prunedResult.parsing_res_list - var result = msg.ContainsKey("result") ? msg["result"] as Dictionary : 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; - var pruned = first != null && first.ContainsKey("prunedResult") - ? first["prunedResult"] as Dictionary : 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 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("]*>(.*?)", - 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("]*>(.*?)", - System.Text.RegularExpressions.RegexOptions.Singleline | - System.Text.RegularExpressions.RegexOptions.IgnoreCase | - System.Text.RegularExpressions.RegexOptions.Compiled); - - /// 表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。 - private static void AddTableItems(List items, string tableHtml, - float x1, float y1, float x2, float y2) - { - var rows = new List(); - foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml)) - { - var cells = new List(); - 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); - } - } -} diff --git a/pdfbt/SelfTest.cs b/pdfbt/SelfTest.cs index 915a3ba..268744d 100644 --- a/pdfbt/SelfTest.cs +++ b/pdfbt/SelfTest.cs @@ -10,7 +10,7 @@ using System.Drawing.Imaging; using System.IO; using System.Text; using System.Threading.Tasks; - +using 成套报价软件; // ★ A1:OCR/PDF 基础设施已链接共享主项目源码(类型在 成套报价软件 命名空间) namespace pdfbt { internal static class SelfTest diff --git a/pdfbt/UapisOcrClient.cs b/pdfbt/UapisOcrClient.cs deleted file mode 100644 index 48903ba..0000000 --- a/pdfbt/UapisOcrClient.cs +++ /dev/null @@ -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 -{ - /// UApiPro 通用 OCR 客户端(单实例)。 - internal sealed class UapisOcrClient - { - private readonly JavaScriptSerializer _json = new JavaScriptSerializer(); - private string _apiKey; - private const string BaseUrl = "https://uapis.cn/api/v1/image/ocr"; - - /// 状态提示,接 OcrClient.StatusChanged。 - public event Action StatusChanged; - - public UapisOcrClient() - { - try - { - ServicePointManager.SecurityProtocol = - ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; - } - catch { } - } - - public bool IsConfigured - { - get { return !string.IsNullOrWhiteSpace(_apiKey); } - } - - /// Key 由 OcrClient 从 baidu_ocr.json 的 uapis_api_key 注入(热更新)。 - public void SetApiKey(string key) - { - _apiKey = key == null ? "" : key.Trim(); - } - - /// 识别一块位图 → 文本行列表(带位置)。multipart 上传。 - public Task> RecognizeAsync(Bitmap crop, int timeoutMs) - { - if (crop == null || crop.Width <= 0 || crop.Height <= 0) - return Task.FromResult(new List()); - - if (!IsConfigured) - { - Notify("UApiPro OCR 未配置 API Key(OCR 设置窗口里填 UApiPro Key)"); - var tcs0 = new TaskCompletionSource>(); - tcs0.SetException(new Exception( - "UApiPro(uapis.cn) OCR 未配置 API Key。\r\n" + - "请到 uapis.cn/console 注册生成 API Key," + - "在 OCR 设置窗口选择引擎\"UApiPro 通用OCR\"并填入后保存。")); - return tcs0.Task; - } - - return Task.Factory.StartNew(() => - { - var sw = System.Diagnostics.Stopwatch.StartNew(); - var result = RequestAndParse(crop, timeoutMs); - Notify("UApiPro OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms"); - return result; - }); - } - - private List RequestAndParse(Bitmap crop, int timeoutMs) - { - byte[] png; - using (var ms = new MemoryStream()) - { - crop.Save(ms, ImageFormat.Png); - png = ms.ToArray(); - } - string b64 = Convert.ToBase64String(png); - - // multipart/form-data 手工拼装(net48 无 MultipartFormDataContent) - string boundary = "----pdfbt" + Guid.NewGuid().ToString("N"); - var body = new MemoryStream(); - Action field = (name, value) => - { - byte[] part = Encoding.UTF8.GetBytes( - "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + - name + "\"\r\n\r\n" + value + "\r\n"); - body.Write(part, 0, part.Length); - }; - field("image_base64", b64); - field("need_location", "true"); - field("enable_cls", "true"); // 文字方向校正(歪扫图抗性) - byte[] tail = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n"); - body.Write(tail, 0, tail.Length); - - var req = (HttpWebRequest)WebRequest.Create(BaseUrl); - req.Method = "POST"; - req.ContentType = "multipart/form-data; boundary=" + boundary; - req.Headers.Add("X-API-Key", _apiKey); - req.Timeout = timeoutMs; - req.ReadWriteTimeout = timeoutMs; - req.Proxy = null; - byte[] payload = body.ToArray(); - req.ContentLength = payload.Length; - using (var rs = req.GetRequestStream()) - { - rs.Write(payload, 0, payload.Length); - } - - string respText; - try - { - using (var resp = (HttpWebResponse)req.GetResponse()) - using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false))) - { - respText = sr.ReadToEnd(); - } - } - catch (WebException wex) - { - string detail = ""; - try - { - if (wex.Response != null) - using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false))) - detail = sr.ReadToEnd(); - } - catch { } - string hint = wex.Response == null - ? ("网络不可达: " + wex.Message) - : ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail); - throw new Exception("UApiPro OCR 请求失败: " + hint); - } - - return ParseResponse(respText, crop.Width, crop.Height); - } - - private List ParseResponse(string respText, int cropW, int cropH) - { - var msg = _json.Deserialize>(respText); - if (msg == null) throw new Exception("UApiPro 响应解析失败(空响应)"); - - // 业务错误:HTTP 200 但 code 非 200/0;或 HTTP 错误体 {code,message} - if (msg.ContainsKey("code")) - { - string code = Convert.ToString(msg["code"]); - if (code != "200" && code != "0" && code != "success") - { - string m = msg.ContainsKey("message") ? Convert.ToString(msg["message"]) : respText; - throw new Exception("UApiPro OCR 错误 [" + code + "]: " + m); - } - } - - var items = new List(); - var arr = msg.ContainsKey("words_result") ? msg["words_result"] as System.Collections.ArrayList : null; - if (arr == null) - throw new Exception("UApiPro 响应没有 words_result(内容: " + - (respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")"); - - foreach (Dictionary w in arr) - { - string text = w.ContainsKey("words") ? Convert.ToString(w["words"]) : ""; - if (string.IsNullOrWhiteSpace(text)) continue; - - var loc = w.ContainsKey("location") ? w["location"] as Dictionary : null; - if (loc == null) continue; - float l = LocF(loc, "left"), t = LocF(loc, "top"); - float wd = LocF(loc, "width"), h = LocF(loc, "height"); - // 裁剪到图内 - l = Math.Max(0, Math.Min(cropW, l)); - t = Math.Max(0, Math.Min(cropH, t)); - wd = Math.Max(2, Math.Min(cropW - l, wd)); - h = Math.Max(2, Math.Min(cropH - t, h)); - - float score = 1f; - if (w.ContainsKey("score")) - { - float sv; - if (float.TryParse(Convert.ToString(w["score"]), out sv)) score = sv; - } - - items.Add(new OcrItem - { - Text = text, - Box = new[] - { - new PointF(l, t), - new PointF(l + wd, t), - new PointF(l + wd, t + h), - new PointF(l, t + h) - }, - Score = score - }); - } - return items; - } - - private static float LocF(Dictionary d, string key) - { - if (d == null || !d.ContainsKey(key)) return 0f; - float v; - if (float.TryParse(Convert.ToString(d[key]), out v)) return v; - return 0f; - } - - private void Notify(string s) - { - var h = StatusChanged; - if (h != null) h(s); - } - } -} diff --git a/pdfbt/UmiOcrClient.cs b/pdfbt/UmiOcrClient.cs deleted file mode 100644 index c180427..0000000 --- a/pdfbt/UmiOcrClient.cs +++ /dev/null @@ -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 -{ - /// Umi-OCR 本地引擎客户端(单实例)。 - internal sealed class UmiOcrClient - { - private readonly JavaScriptSerializer _json = new JavaScriptSerializer(); - private string _baseUrl = "http://127.0.0.1:1224"; - - /// 状态提示,接 OcrClient.StatusChanged。 - public event Action StatusChanged; - - public bool IsConfigured - { - get { return true; } // 本地引擎无需 Key - } - - /// 服务地址由 OcrClient 从 baidu_ocr.json 的 umi_url 注入(热更新)。 - 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('/'); - } - - /// 识别一块位图 → 文本行列表(带位置)。JSON POST 到本机 Umi-OCR。 - public Task> RecognizeAsync(Bitmap crop, int timeoutMs) - { - if (crop == null || crop.Width <= 0 || crop.Height <= 0) - return Task.FromResult(new List()); - - 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 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 - { - { "base64", b64 }, - { "options", new Dictionary { { "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>(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(); // 无文本,不算错误 - if (code != 100) - throw new Exception("Umi-OCR 错误 code=" + code + ": " + - Convert.ToString(resp.ContainsKey("data") ? resp["data"] : "")); - - var items = new List(); - var arr = resp.ContainsKey("data") ? resp["data"] as System.Collections.ArrayList : null; - if (arr != null) - { - foreach (Dictionary 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); - } - } -} diff --git a/pdfbt/UmiOcrHost.cs b/pdfbt/UmiOcrHost.cs deleted file mode 100644 index 9162f24..0000000 --- a/pdfbt/UmiOcrHost.cs +++ /dev/null @@ -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 - { - /// 本体 zip 直链(用户网盘直连,GET 才有内容,HEAD 会 404,不支持断点续传)。 - public const string DownloadUrl = "http://s333.odn.cc/s/845f64a239b665ec"; - - private const string InstallDirName = "Umi-OCR"; - private const int HttpPort = 1224; - - /// 由本程序启动的实例(停止时只关它,不碰用户自己开的)。 - private static Process _startedProc; - - /// 本体安装目录(exe 旁 Umi-OCR\)。 - public static string InstallDir - { - get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, InstallDirName); } - } - - /// 找安装目录里的主程序(Umi-OCR.exe,大小写不敏感;找不到返回 null)。 - 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; } - } - - /// 本体是否已安装(目录里能找到主程序)。 - public static bool IsInstalled - { - get { return FindExe() != null; } - } - - /// HTTP 服务(1224 端口)是否在监听。 - 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; - } - - /// 三态:0=未安装,1=已安装未运行,2=运行中(HTTP 在线)。 - public static int Detect() - { - if (!IsInstalled) return 0; - return IsHttpUp() ? 2 : 1; - } - - /// 状态文字(设置窗口显示用)。 - public static string DetectText() - { - int st = Detect(); - if (st == 0) return "未检测到 OCR 本体(点\"下载 OCR 本体\"自动下载安装)"; - if (st == 1) return "已检测到 OCR 本体,服务未运行(识别时会自动启动)"; - return "OCR 本体运行中(HTTP " + HttpPort + " 端口在线)"; - } - - /// 下载 zip → 解压到 Umi-OCR\ → 验证主程序 → 删 zip。 - /// progress: 0~100 下载百分比;解压与收尾阶段回调 100。失败抛异常(内部已清理残留)。 - /// ★ 必须用 DownloadFileAsync:同步版 DownloadFile 不触发 DownloadProgressChanged - /// (进度事件只在异步路径发,同步下载会一直停在 0%)。 - public static Task DownloadAndInstallAsync(Action 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(); - 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; - } - }); - } - - /// 确保本体已安装且 HTTP 服务在线:未安装抛错引导下载;未运行则静默启动(--hide 主窗口进托盘)并等端口就绪(≤15秒)。 - /// 返回 false=已安装但启动超时(调用方提示)。 - 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; - } - - /// 停止由本程序启动的实例(PdfViewerWindow 关闭时调用;用户自己开的不动)。 - /// --hide 启动的窗口在托盘,CloseMainWindow 不可靠:先发官方 --quit 指令优雅退出,超时再 Kill。 - 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 { } } - } - } -} diff --git a/pdfbt/pdfbt.csproj b/pdfbt/pdfbt.csproj index 224661a..40c630c 100644 --- a/pdfbt/pdfbt.csproj +++ b/pdfbt/pdfbt.csproj @@ -1,4 +1,4 @@ - + {7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1} @@ -113,16 +113,43 @@ - - - - - - - - - - + + + 共享扒图\PdfiumNative.cs + + + 共享扒图\PdfDocument.cs + + + 共享扒图\OcrClient.cs + + + 共享扒图\GlmOcrClient.cs + + + 共享扒图\QianfanOcrClient.cs + + + 共享扒图\UapisOcrClient.cs + + + 共享扒图\UmiOcrClient.cs + + + 共享扒图\UmiOcrHost.cs + + + 共享扒图\UmiOcrLauncher.cs + + + 共享扒图\OcrSettingsForm.cs + + + 共享扒图\OcrSettingsForm.Designer.cs + OcrSettingsForm.cs + @@ -155,9 +182,7 @@ MainForm.cs - - OcrSettingsForm.Designer.cs - + Form3.cs diff --git a/tools/verify_build.rsp b/tools/verify_build.rsp index 1f3ea1b..5ce8d7f 100644 --- a/tools/verify_build.rsp +++ b/tools/verify_build.rsp @@ -79,6 +79,7 @@ shezhi\许可证文本.Designer.cs 登录.Designer.cs 会员服务Host.cs OpenClawHost.cs +数据库连接.cs AI助手接口.cs AI助手设置.cs AI助手设置.Designer.cs diff --git a/成套报价软件/xiangmu/导入报价.cs b/成套报价软件/xiangmu/导入报价.cs index 2a95ad7..ec5c043 100644 --- a/成套报价软件/xiangmu/导入报价.cs +++ b/成套报价软件/xiangmu/导入报价.cs @@ -48,7 +48,8 @@ namespace 成套报价软件 InitializeComponent(); _projectId = projectId; _dbPath = dbPath; - _connString = "Data Source=" + dbPath + ";Version=3;"; + // ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串 + _connString = 数据库连接.串(dbPath); // 绑定事件 this.button1.Click += button1_Click; diff --git a/成套报价软件/xiangmu/甲方报表.cs b/成套报价软件/xiangmu/甲方报表.cs index e88553b..7c1db89 100644 --- a/成套报价软件/xiangmu/甲方报表.cs +++ b/成套报价软件/xiangmu/甲方报表.cs @@ -305,7 +305,8 @@ namespace 成套报价软件 private string GetConnString(string dbPath) { - return "Data Source=" + dbPath + ";Version=3;"; + // ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串 + return 数据库连接.串(dbPath); } private Dictionary LoadProjectInfo() diff --git a/成套报价软件/xiangmu/调系数.cs b/成套报价软件/xiangmu/调系数.cs index 484925b..9df6ffe 100644 --- a/成套报价软件/xiangmu/调系数.cs +++ b/成套报价软件/xiangmu/调系数.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Globalization; @@ -503,7 +503,8 @@ namespace 成套报价软件 private string GetConnString(string dbPath) { - return "Data Source=" + dbPath + ";Version=3;"; + // ★ A2(2026-10-01):原为裸串(无 WAL/超时,并发易锁死)——统一收敛到 数据库连接.串 + return 数据库连接.串(dbPath); } /// 查表索引(复用连接版本)。 diff --git a/成套报价软件/主窗口.cs b/成套报价软件/主窗口.cs index 8045c7f..20fd3e9 100644 --- a/成套报价软件/主窗口.cs +++ b/成套报价软件/主窗口.cs @@ -3663,9 +3663,8 @@ namespace 成套报价软件 private string GetConnString(string dbPath) { - // ★ B4(2026-10-01):加 BusyTimeout=5000——直写队列/全库重算/扒图窗口并发访问同一库时, - // 遇锁默认 0 等待直接抛 database is locked;加超时后 SQLite 自动等待重试(方案窗口早已同款) - return "Data Source=" + dbPath + ";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;"; + // ★ A2(2026-10-01):统一收敛到 数据库连接.串(一处管理),本方法保留签名做转发,调用方零改动 + return 数据库连接.串(dbPath); } /// diff --git a/成套报价软件/主窗口.菜单与库.cs b/成套报价软件/主窗口.菜单与库.cs index f1b1fcc..58584ca 100644 --- a/成套报价软件/主窗口.菜单与库.cs +++ b/成套报价软件/主窗口.菜单与库.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Diagnostics; @@ -106,11 +106,11 @@ namespace 成套报价软件 return list; } - /// GetConnString 的静态包装,供静态方法调用。 + /// GetConnString 的静态包装,供静态方法调用。 + /// ★ A2(2026-10-01):统一收敛到 数据库连接.串(一处管理),本方法保留签名做转发。 private static string GetConnStringStatic(string dbPath) { - // ★ B4(2026-10-01):与实例版同款加 BusyTimeout(防并发锁直报错) - return "Data Source=\"" + dbPath + "\";Version=3;Journal Mode=WAL;Synchronous=Normal;Pooling=True;BusyTimeout=5000;"; + return 数据库连接.串(dbPath); } /// diff --git a/成套报价软件/成套报价软件.csproj b/成套报价软件/成套报价软件.csproj index 2a2a1b8..27b1795 100644 --- a/成套报价软件/成套报价软件.csproj +++ b/成套报价软件/成套报价软件.csproj @@ -1,4 +1,4 @@ - + {B6966FA3-E161-44ED-8A40-CCDBE1F15192} @@ -159,6 +159,7 @@ + diff --git a/成套报价软件/扒图/Cad/Form3.cs b/成套报价软件/扒图/Cad/Form3.cs index 9a10a32..f137911 100644 --- a/成套报价软件/扒图/Cad/Form3.cs +++ b/成套报价软件/扒图/Cad/Form3.cs @@ -612,9 +612,8 @@ namespace MyApp private string GetConnString(string dbPath) { - // ★ busy_timeout=3000:数据库被 CAD 或其他进程短暂占用时等待重试而非卡死; - // Pooling 关闭,避免连接池长期持有文件句柄 - return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;"; + // ★ A2(2026-10-01):统一收敛到主程序 数据库连接.串无池(保留扒图关池语义,一处管理) + return 成套报价软件.数据库连接.串无池(dbPath); } private string GetQuoteDbPath(string projectId) diff --git a/成套报价软件/扒图/Pdf/Form3.cs b/成套报价软件/扒图/Pdf/Form3.cs index 8b3b71d..5786a62 100644 --- a/成套报价软件/扒图/Pdf/Form3.cs +++ b/成套报价软件/扒图/Pdf/Form3.cs @@ -635,9 +635,8 @@ namespace MyAppPdf private string GetConnString(string dbPath) { - // ★ busy_timeout=3000:数据库被 CAD 或其他进程短暂占用时等待重试而非卡死; - // Pooling 关闭,避免连接池长期持有文件句柄 - return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;"; + // ★ A2(2026-10-01):统一收敛到主程序 数据库连接.串无池(保留扒图关池语义,一处管理) + return 成套报价软件.数据库连接.串无池(dbPath); } private string GetQuoteDbPath(string projectId) diff --git a/成套报价软件/数据库连接.cs b/成套报价软件/数据库连接.cs new file mode 100644 index 0000000..cbb7466 --- /dev/null +++ b/成套报价软件/数据库连接.cs @@ -0,0 +1,30 @@ +using System; + +namespace 成套报价软件 +{ + /// SQLite 连接字符串统一管理(A2,2026-10-01):全项目连接参数一处调整全局生效。 + /// 此前 6 份重复定义(主窗口/菜单与库/调系数/甲方报表/Cad扒图/Pdf扒图)参数漂移—— + /// 调系数与甲方报表是裸串(无 WAL/超时,并发易锁死),扒图窗口是有意关池(防句柄常驻)。 + /// 现收敛为两个变体:各处私有 GetConnString 均转发到这里,调用方零改动。 + public static class 数据库连接 + { + /// 标准连接串(主窗口/直写队列/重算/报表等主程序各模块用): + /// WAL+Normal 同步+连接池+5秒锁等待。项目库文件首开后即持久保持 WAL 模式, + /// 各连接串里是否声明 Journal Mode 只影响新库,对已建库无实际差异。 + 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;"; + } + + /// 无池连接串(扒图窗口 Cad/Pdf Form3 专用): + /// ★ Pooling 关闭是该窗口的历史有意设计——避免连接池长期持有库文件句柄 + /// (导出项目复制库/删库等文件操作不受池句柄干扰),收敛时保留该语义; + /// busy_timeout=3000:库被其他模块短暂占用时等待重试而非卡死。 + public static string 串无池(string dbPath) + { + return "Data Source=" + dbPath + ";Version=3;busy_timeout=3000;"; + } + } +}