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;";
+ }
+ }
+}