基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# 新增 UApiPro(uapis.cn) 通用文字识别引擎(第五个 OCR 引擎)
|
||||
|
||||
## 已确认的接口细节(官方文档 + 官网已核实)
|
||||
- 端点:`POST https://uapis.cn/api/v1/image/ocr`,multipart/form-data
|
||||
- 鉴权:`X-API-Key: <key>`(uapis.cn/console 邮箱注册即生成,免费积分 4积分/次,限速 4QPS)
|
||||
- 请求字段:`image_base64`(纯 base64 或 DataURI)+ `need_location="true"`(返回坐标)+ `enable_cls="true"`(方向校正)
|
||||
- 响应:`words_result[]{words, location{left,top,width,height}, score}` ——**每行带位置和置信度**,结构与百度OCR几乎一致
|
||||
- 布尔参数是字符串形式;图片 ≤10MB
|
||||
|
||||
## 修改文件
|
||||
|
||||
### 1. 新增 UapisOcrClient.cs
|
||||
- multipart/form-data 手工拼装(net48 无 MultipartFormDataContent)
|
||||
- words_result 解析 → OcrItem(location 像素坐标直接用,score 保留供图形符号过滤用)
|
||||
- 错误处理:HTTP 错误体 {code,message} 与业务 code 非 200 双查
|
||||
- Key 存 baidu_ocr.json 的 uapis_api_key
|
||||
|
||||
### 2. OcrClient.cs 路由加 "uapis"
|
||||
- 新增 _uapis 成员 + StatusChanged 转发 + LoadConfig 注入 Key + RecognizeAsync 路由
|
||||
|
||||
### 3. OcrSettingsForm 第四个引擎选项
|
||||
- 下拉加"UApiPro 通用OCR(免费积分/带位置)";选中显示 UApiPro Key 输入行;保存 uapis_api_key
|
||||
|
||||
### 4. csproj 注册 → 编译 → 关闭重启 → 交付说明
|
||||
|
||||
## 说明
|
||||
- 该引擎带 score 置信度,图形符号过滤(<0.8 短串拦截、<0.55 长文本兜底)对它自动生效
|
||||
- 4QPS 限速对人工框选节奏完全够用;免费积分计费
|
||||
- 五引擎格局:百度(内置Key即用)/ 智谱GLM / 千帆PaddleOCR-VL / UApiPro,外加本地GPU rapidocr 已退役
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
namespace pdfbt.DockWindows
|
||||
{
|
||||
partial class QuoteWindow
|
||||
{
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null)) components.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Designer generated code
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// QuoteWindow
|
||||
//
|
||||
this.Name = "QuoteWindow";
|
||||
this.ResumeLayout(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// ============================================================
|
||||
// 停靠窗口:报价插件工具栏(左侧)
|
||||
// 把 cadcj 的 Form3 报价界面作为子控件嵌入(TopLevel=false)。
|
||||
// ============================================================
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace pdfbt.DockWindows
|
||||
{
|
||||
/// <summary>停靠窗口:报价插件工具栏(DockState.DockLeft)。</summary>
|
||||
public partial class QuoteWindow : WeifenLuo.WinFormsUI.Docking.DockContent
|
||||
{
|
||||
public QuoteWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Text = "报价工具";
|
||||
TabText = "报价工具";
|
||||
|
||||
var form = new MyApp.Form3();
|
||||
form.TopLevel = false;
|
||||
form.FormBorderStyle = FormBorderStyle.None;
|
||||
form.Dock = DockStyle.Fill;
|
||||
Controls.Add(form);
|
||||
form.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
// ============================================================
|
||||
// GlmOcrClient:智谱 GLM-OCR 云端接口(版面解析 layout_parsing)
|
||||
// · POST {BaseUrl} Authorization: Bearer {ApiKey}
|
||||
// {"model":"glm-ocr","file":"data:image/png;base64,..."}
|
||||
// · 响应 layout_details[页][{label,bbox_2d(归一化),content,width,height}]
|
||||
// → label=text 的块转 OcrItem(bbox 映射回裁剪图像素坐标)
|
||||
// · 手写字识别(模型内置,无需专用参数);价格 ~0.2 元/百万 tokens
|
||||
// · 纯 C# HTTP,无需 Python;引擎切换由 OcrClient 路由
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>智谱 GLM-OCR 客户端(单实例)。</summary>
|
||||
internal sealed class GlmOcrClient
|
||||
{
|
||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
||||
private string _apiKey;
|
||||
private string _baseUrl;
|
||||
|
||||
/// <summary>状态提示(未配置Key/请求失败等),接 OcrClient.StatusChanged。</summary>
|
||||
public event Action<string> StatusChanged;
|
||||
|
||||
public GlmOcrClient()
|
||||
{
|
||||
_baseUrl = ReadAppSetting("GlmBaseUrl",
|
||||
"https://open.bigmodel.cn/api/paas/v4/layout_parsing");
|
||||
try
|
||||
{
|
||||
// net48 下显式启用 TLS1.2(大模型 API 必须)
|
||||
ServicePointManager.SecurityProtocol =
|
||||
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 glm_api_key 注入(热更新)。</summary>
|
||||
public void SetApiKey(string key)
|
||||
{
|
||||
_apiKey = key == null ? "" : key.Trim();
|
||||
}
|
||||
|
||||
public bool IsConfigured
|
||||
{
|
||||
get { return !string.IsNullOrWhiteSpace(_apiKey); }
|
||||
}
|
||||
|
||||
private static string ReadAppSetting(string key, string def)
|
||||
{
|
||||
try
|
||||
{
|
||||
var v = System.Configuration.ConfigurationManager.AppSettings[key];
|
||||
return string.IsNullOrWhiteSpace(v) ? def : v;
|
||||
}
|
||||
catch { return def; }
|
||||
}
|
||||
|
||||
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</summary>
|
||||
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
|
||||
return Task.FromResult(new List<OcrItem>());
|
||||
|
||||
if (!IsConfigured)
|
||||
{
|
||||
Notify("GLM OCR 未配置 API Key(OCR 设置窗口里填智谱 API Key)");
|
||||
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
|
||||
tcs0.SetException(new Exception(
|
||||
"智谱 GLM-OCR 未配置 API Key。\r\n请到 open.bigmodel.cn 获取 Key," +
|
||||
"在 OCR 设置窗口选择引擎\"智谱 GLM-OCR\"并填入 Key 后保存。"));
|
||||
return tcs0.Task;
|
||||
}
|
||||
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
var result = RequestAndParse(crop, timeoutMs);
|
||||
Notify("智谱GLM-OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
byte[] png;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
crop.Save(ms, ImageFormat.Png);
|
||||
png = ms.ToArray();
|
||||
}
|
||||
// 服务端期望 data URI 或 URL(SDK 自动包装 data URI,此处显式包装)
|
||||
string fileField = "data:image/png;base64," + Convert.ToBase64String(png);
|
||||
|
||||
string body = _json.Serialize(new Dictionary<string, object>
|
||||
{
|
||||
{ "model", "glm-ocr" },
|
||||
{ "file", fileField }
|
||||
});
|
||||
|
||||
var req = (HttpWebRequest)WebRequest.Create(_baseUrl);
|
||||
req.Method = "POST";
|
||||
req.ContentType = "application/json";
|
||||
req.Headers.Add("Authorization", "Bearer " + _apiKey);
|
||||
req.Timeout = timeoutMs;
|
||||
req.ReadWriteTimeout = timeoutMs;
|
||||
req.Proxy = null; // 尊重直连,避免系统代理拖慢
|
||||
var bytes = Encoding.UTF8.GetBytes(body);
|
||||
req.ContentLength = bytes.Length;
|
||||
using (var rs = req.GetRequestStream())
|
||||
{
|
||||
rs.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
string respText;
|
||||
try
|
||||
{
|
||||
using (var resp = (HttpWebResponse)req.GetResponse())
|
||||
using (var sr = new StreamReader(resp.GetResponseStream(),
|
||||
new UTF8Encoding(false)))
|
||||
{
|
||||
respText = sr.ReadToEnd();
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
string detail = "";
|
||||
try
|
||||
{
|
||||
if (wex.Response != null)
|
||||
using (var sr = new StreamReader(wex.Response.GetResponseStream(),
|
||||
new UTF8Encoding(false)))
|
||||
detail = sr.ReadToEnd();
|
||||
}
|
||||
catch { }
|
||||
string hint = wex.Response == null
|
||||
? ("网络不可达: " + wex.Message)
|
||||
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
|
||||
throw new Exception("GLM OCR 请求失败: " + hint);
|
||||
}
|
||||
|
||||
return ParseResponse(respText, crop.Width, crop.Height);
|
||||
}
|
||||
|
||||
/// <summary>解析 layout_parsing 响应 → OcrItem 列表(text 块)。</summary>
|
||||
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
|
||||
{
|
||||
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
|
||||
if (msg == null) throw new Exception("GLM 响应解析失败(空响应)");
|
||||
if (msg.ContainsKey("error"))
|
||||
{
|
||||
var err = msg["error"] as Dictionary<string, object>;
|
||||
string code = err != null && err.ContainsKey("code") ? Convert.ToString(err["code"]) : "?";
|
||||
string message = err != null && err.ContainsKey("message") ? Convert.ToString(err["message"]) : respText;
|
||||
throw new Exception("GLM OCR 错误 [" + code + "]: " + message);
|
||||
}
|
||||
|
||||
var items = new List<OcrItem>();
|
||||
var pages = msg.ContainsKey("layout_details")
|
||||
? msg["layout_details"] as System.Collections.ArrayList : null;
|
||||
if (pages == null || pages.Count == 0)
|
||||
throw new Exception("GLM 响应没有 layout_details(内容: " +
|
||||
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
|
||||
|
||||
// 单页输入 → 取第一页的块
|
||||
var blocks = pages[0] as System.Collections.ArrayList;
|
||||
if (blocks == null) return items;
|
||||
|
||||
foreach (Dictionary<string, object> b in blocks)
|
||||
{
|
||||
string label = b.ContainsKey("label") ? Convert.ToString(b["label"]) : "";
|
||||
string content = b.ContainsKey("content") ? Convert.ToString(b["content"]) : "";
|
||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||
|
||||
var bbox = b.ContainsKey("bbox_2d") ? b["bbox_2d"] as System.Collections.ArrayList : null;
|
||||
if (bbox == null || bbox.Count < 4) continue;
|
||||
|
||||
// ★ 实测返回像素坐标;官方文档描述归一化(0~1)。按值域自动兼容:
|
||||
// 四个值全部 ≤1.5 → 归一化,乘图尺寸;否则直接当像素用。
|
||||
float a0 = Convert.ToSingle(bbox[0]), a1 = Convert.ToSingle(bbox[1]);
|
||||
float a2 = Convert.ToSingle(bbox[2]), a3 = Convert.ToSingle(bbox[3]);
|
||||
bool normalized = a0 <= 1.5f && a1 <= 1.5f && a2 <= 1.5f && a3 <= 1.5f;
|
||||
float x1 = normalized ? a0 * cropW : a0;
|
||||
float y1 = normalized ? a1 * cropH : a1;
|
||||
float x2 = normalized ? a2 * cropW : a2;
|
||||
float y2 = normalized ? a3 * cropH : a3;
|
||||
// 裁剪到图内(超界坐标画在页面外会导致蒙版不可见)
|
||||
x1 = Math.Max(0, Math.Min(cropW, x1));
|
||||
y1 = Math.Max(0, Math.Min(cropH, y1));
|
||||
x2 = Math.Max(0, Math.Min(cropW, x2));
|
||||
y2 = Math.Max(0, Math.Min(cropH, y2));
|
||||
|
||||
if (label == "table")
|
||||
{
|
||||
// ★ 框选多行文字常被判定为表格块(content 为 <table> HTML):
|
||||
// 每个 <tr> 一条文本(单元格空格拼接),行高均分定位。
|
||||
// 必须在 CleanHtml 剥标签之前解析结构。
|
||||
AddTableItems(items, content, x1, y1, x2, y2);
|
||||
continue;
|
||||
}
|
||||
if (label != "text") continue; // 图片/公式块跳过
|
||||
content = CleanHtmlShared(content); // 居中块会返回 <div align=..>..</div> 等 HTML
|
||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||
|
||||
// 多行文本块拆行(content 按 \n,块内均匀分布);清理 Markdown 标记
|
||||
// ★ 行间上下各收缩 15%(≤3px):蒙版之间留空隙,不连成一片
|
||||
string[] lines = content.Split(new[] { '\r', '\n' },
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
float lineH = (y2 - y1) / lines.Length;
|
||||
float pad = Math.Min(lineH * 0.15f, 3f);
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
string text = CleanMarkdownShared(lines[i]).Trim();
|
||||
if (text.Length == 0) continue;
|
||||
float ty1 = y1 + lineH * i + pad;
|
||||
float ty2 = y1 + lineH * (i + 1) - pad;
|
||||
if (ty2 - ty1 < 2) { ty1 = y1 + lineH * i; ty2 = ty1 + Math.Max(2, lineH); }
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = text,
|
||||
Box = new[]
|
||||
{
|
||||
new PointF(x1, ty1),
|
||||
new PointF(x2, ty1),
|
||||
new PointF(x2, ty2),
|
||||
new PointF(x1, ty2)
|
||||
},
|
||||
Score = 1f
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex _trRegex =
|
||||
new System.Text.RegularExpressions.Regex("<tr[^>]*>(.*?)</tr>",
|
||||
System.Text.RegularExpressions.RegexOptions.Singleline |
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
private static readonly System.Text.RegularExpressions.Regex _tdRegex =
|
||||
new System.Text.RegularExpressions.Regex("<t[dh][^>]*>(.*?)</t[dh]>",
|
||||
System.Text.RegularExpressions.RegexOptions.Singleline |
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
|
||||
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
|
||||
private static void AddTableItems(List<OcrItem> items, string tableHtml,
|
||||
float x1, float y1, float x2, float y2)
|
||||
{
|
||||
var rows = new List<string>();
|
||||
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
|
||||
{
|
||||
var cells = new List<string>();
|
||||
foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value))
|
||||
{
|
||||
string cell = CleanHtmlShared(td.Groups[1].Value).Trim();
|
||||
if (cell.Length > 0) cells.Add(cell);
|
||||
}
|
||||
if (cells.Count > 0) rows.Add(string.Join(" ", cells.ToArray()));
|
||||
}
|
||||
if (rows.Count == 0) return;
|
||||
float rowH = (y2 - y1) / rows.Count;
|
||||
float pad = Math.Min(rowH * 0.15f, 3f); // 行间留空隙,蒙版不连片
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
float ty1 = y1 + rowH * i + pad;
|
||||
float ty2 = y1 + rowH * (i + 1) - pad;
|
||||
if (ty2 - ty1 < 2) { ty1 = y1 + rowH * i; ty2 = ty1 + Math.Max(2, rowH); }
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = rows[i],
|
||||
Box = new[]
|
||||
{
|
||||
new PointF(x1, ty1),
|
||||
new PointF(x2, ty1),
|
||||
new PointF(x2, ty2),
|
||||
new PointF(x1, ty2)
|
||||
},
|
||||
Score = 1f
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex _htmlTag =
|
||||
new System.Text.RegularExpressions.Regex("<[^>]*>",
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// 剥离 GLM content 里的 HTML 片段(居中块返回 div 等)与常见实体;
|
||||
/// 标签替换为空格,避免前后文字误粘连;连续空格合并。
|
||||
/// </summary>
|
||||
internal static string CleanHtmlShared(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return s;
|
||||
if (s.IndexOf('<') >= 0)
|
||||
{
|
||||
s = _htmlTag.Replace(s, " ");
|
||||
s = s.Replace(" ", " ").Replace("&", "&")
|
||||
.Replace("<", "<").Replace(">", ">")
|
||||
.Replace(""", "\"").Replace("'", "'");
|
||||
}
|
||||
while (s.Contains(" ")) s = s.Replace(" ", " ");
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理 GLM 返回 content 里的 Markdown 标记(★保守规则,不伤正文):
|
||||
/// · 行首 "##"/"#" 标题井号(仅行首;"2#配电箱" 中间的 # 保留)
|
||||
/// · 成对 **加粗** 星号、行首 "- "/"* " 列表标记
|
||||
/// </summary>
|
||||
internal static string CleanMarkdownShared(string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) return s;
|
||||
// 行首标题:# 开头且后跟空格/更多#/行尾 → 剥掉行首所有 #
|
||||
string t = s.TrimStart();
|
||||
if (t.StartsWith("#"))
|
||||
{
|
||||
int i = 0;
|
||||
while (i < t.Length && t[i] == '#') i++;
|
||||
// "#..." 后是空格或行尾才视为标题标记
|
||||
if (i >= t.Length || t[i] == ' ' || t[i] == '\t')
|
||||
t = t.Substring(i).TrimStart();
|
||||
// 否则(如 "#5 线缆")保留原样
|
||||
else t = s;
|
||||
}
|
||||
// 行首列表标记
|
||||
if (t.StartsWith("- ") || t.StartsWith("* "))
|
||||
t = t.Substring(2);
|
||||
// 成对 ** (加粗) 去星号
|
||||
if (t.Contains("**"))
|
||||
t = t.Replace("**", "");
|
||||
return t;
|
||||
}
|
||||
|
||||
private void Notify(string s)
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+71
@@ -0,0 +1,71 @@
|
||||
namespace pdfbt
|
||||
{
|
||||
partial class MainForm
|
||||
{
|
||||
/// <summary>
|
||||
/// Designer variable used to keep track of non-visual components.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources used by the form.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) {
|
||||
if (components != null) {
|
||||
components.Dispose();
|
||||
}
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is required for Windows Forms designer support.
|
||||
/// Do not change the method contents inside the source code editor.
|
||||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
Smart.CustomComponents.DockWindows.DockWindowItem dockWindowItem1 = new Smart.CustomComponents.DockWindows.DockWindowItem();
|
||||
Smart.CustomComponents.DockWindows.DockWindowItem dockWindowItem2 = new Smart.CustomComponents.DockWindows.DockWindowItem();
|
||||
this.dockSurface1 = new Smart.CustomComponents.DockSurface();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// dockSurface1
|
||||
//
|
||||
this.dockSurface1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||
this.dockSurface1.DockBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(238)))), ((int)(((byte)(238)))), ((int)(((byte)(242)))));
|
||||
dockWindowItem1.ClassName = "QuoteWindow";
|
||||
dockWindowItem1.DefaultWidth = 440;
|
||||
dockWindowItem1.ShowCloseButton = false;
|
||||
dockWindowItem1.Title = "报价工具";
|
||||
dockWindowItem2.ClassName = "PdfViewerWindow";
|
||||
dockWindowItem2.DockState = WeifenLuo.WinFormsUI.Docking.DockState.Document;
|
||||
dockWindowItem2.ShowCloseButton = false;
|
||||
dockWindowItem2.Title = "PDF 查看器";
|
||||
this.dockSurface1.DockWindows.Add(dockWindowItem1);
|
||||
this.dockSurface1.DockWindows.Add(dockWindowItem2);
|
||||
this.dockSurface1.Location = new System.Drawing.Point(0, 0);
|
||||
this.dockSurface1.Name = "dockSurface1";
|
||||
this.dockSurface1.Padding = new System.Windows.Forms.Padding(6);
|
||||
this.dockSurface1.ShowAutoHideContentOnHover = false;
|
||||
this.dockSurface1.Size = new System.Drawing.Size(1184, 661);
|
||||
this.dockSurface1.TabIndex = 0;
|
||||
//
|
||||
// MainForm
|
||||
//
|
||||
this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 18F);
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||
this.ClientSize = new System.Drawing.Size(1184, 661);
|
||||
this.Controls.Add(this.dockSurface1);
|
||||
this.Name = "MainForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "PDF 扒图";
|
||||
this.ResumeLayout(false);
|
||||
|
||||
}
|
||||
|
||||
private Smart.CustomComponents.DockSurface dockSurface1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// 本文件由『生成停靠窗口代码』自动生成格式参考手写,集合变更后请同步刷新。
|
||||
using System;
|
||||
using WeifenLuo.WinFormsUI.Docking;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
public partial class MainForm
|
||||
{
|
||||
void CreateDockWindows_dockSurface1()
|
||||
{
|
||||
var dockSurface1 = this.dockSurface1 as Smart.CustomComponents.DockSurface;
|
||||
if (dockSurface1 == null) return;
|
||||
dockSurface1.EnsureDefaultTheme(); // 必须:Show 前保证有效主题
|
||||
|
||||
var quote = new pdfbt.DockWindows.QuoteWindow();
|
||||
quote.CloseButtonVisible = false;
|
||||
quote.TabText = "报价工具";
|
||||
quote.Text = "报价工具";
|
||||
quote.Show(dockSurface1, DockState.DockLeft);
|
||||
|
||||
var viewer = new pdfbt.PdfViewerWindow();
|
||||
viewer.CloseButtonVisible = false;
|
||||
viewer.TabText = "PDF 查看器";
|
||||
viewer.Text = "PDF 查看器";
|
||||
viewer.Show(dockSurface1, DockState.Document);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* MainForm:主窗体(报价工具 + PDF 查看器)
|
||||
* 由 cadbt 改造:WebView2 网页 CAD → pdfium 原生 PDF 查看 + 框选 OCR
|
||||
*/
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
CreateDockWindows_dockSurface1();
|
||||
|
||||
// ★ 全局 F1/F2/F3 过滤器:焦点在任何 WinForms 控件上都触发元件合并
|
||||
// (与 cadbt 一致,两路都汇聚到 PdfBridge.F*Requested)
|
||||
Application.AddMessageFilter(new F1MessageFilter());
|
||||
FormClosed += (s, e) => Application.RemoveMessageFilter(f1Filter);
|
||||
|
||||
// ★ Umi-OCR 预热:已装本体且当前引擎为 umi 时,主程序一启动就静默拉起
|
||||
// (--hide 托盘),不等首次框选;后台线程跑,不拖慢窗口出现
|
||||
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json")))
|
||||
{
|
||||
var jsv = new System.Web.Script.Serialization.JavaScriptSerializer()
|
||||
.Deserialize<System.Collections.Generic.Dictionary<string, string>>(
|
||||
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();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
|
||||
private readonly F1MessageFilter f1Filter = new F1MessageFilter();
|
||||
|
||||
/// <summary>
|
||||
/// 把 F1/F2/F3 按键(0x70/0x71/0x72)转成 PdfBridge 事件。
|
||||
/// ★ 焦点在报价插件(Form3)树内时不触发——那条路径由 Form3.ProcessCmdKey
|
||||
/// 处理,此处再触发会双跑一次(F1 合并两遍:第一遍成功、第二遍弹
|
||||
/// "请先选中至少2行"的误提示)。
|
||||
/// </summary>
|
||||
private class F1MessageFilter : IMessageFilter
|
||||
{
|
||||
public bool PreFilterMessage(ref Message m)
|
||||
{
|
||||
const int WM_KEYDOWN = 0x0100;
|
||||
bool isFKey = m.Msg == WM_KEYDOWN &&
|
||||
((int)m.WParam == 0x70 || (int)m.WParam == 0x71 || (int)m.WParam == 0x72);
|
||||
if (!isFKey) return false;
|
||||
|
||||
// 焦点控件在 Form3 树内 → 交给 Form3.ProcessCmdKey,跳过
|
||||
try
|
||||
{
|
||||
var focused = Control.FromHandle(m.HWnd);
|
||||
for (var c = focused; c != null; c = c.Parent)
|
||||
{
|
||||
if (c is MyApp.Form3) return false;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
int key = (int)m.WParam;
|
||||
try
|
||||
{
|
||||
if (key == 0x70) PdfBridge.RaiseF1();
|
||||
else if (key == 0x71) PdfBridge.RaiseF2();
|
||||
else if (key == 0x72) PdfBridge.RaiseF3();
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,417 @@
|
||||
// ============================================================
|
||||
// OcrClient:OCR 引擎路由(baidu_ocr.json 的 engine 字段选择)
|
||||
// · engine=baidu(默认):百度 OCR 高精度含位置版(HTTP 直连)
|
||||
// · engine=glm:智谱 GLM-OCR 版面解析(手写字强,GlmOcrClient)
|
||||
// · Key 都在 exe 旁 baidu_ocr.json(OCR设置窗口写):
|
||||
// api_key/secret_key=百度;glm_api_key=智谱
|
||||
// 百度未保存过/Key 为空 → 内置默认 Key 直接可用
|
||||
// · 百度 access_token 缓存 exe 旁 baidu_token.json(29 天,提前1天过期)
|
||||
// · 未配 Key / 网络失败直接抛异常(界面提示),无本地回退
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
public class OcrItem
|
||||
{
|
||||
public string Text;
|
||||
public PointF[] Box; // 裁剪图像素坐标,4 点(左上起顺时针)
|
||||
public float Score;
|
||||
|
||||
public RectangleF Bounds
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Box == null || Box.Length == 0) return RectangleF.Empty;
|
||||
float l = Box[0].X, t = Box[0].Y, r = Box[0].X, b = Box[0].Y;
|
||||
foreach (var p in Box)
|
||||
{
|
||||
if (p.X < l) l = p.X; if (p.X > r) r = p.X;
|
||||
if (p.Y < t) t = p.Y; if (p.Y > b) b = p.Y;
|
||||
}
|
||||
return RectangleF.FromLTRB(l, t, r, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class OcrClient : IDisposable
|
||||
{
|
||||
public event Action<string> StatusChanged;
|
||||
|
||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
||||
private string _ak = "", _sk = "";
|
||||
private string _token;
|
||||
private DateTime _tokenExpire = DateTime.MinValue;
|
||||
|
||||
// ★ 引擎路由:baidu(默认)/glm(智谱 GLM-OCR)/qianfan(百度 PaddleOCR-VL)/uapis(UApiPro)/umi(Umi-OCR 本地)
|
||||
private string _engine = "baidu";
|
||||
private readonly GlmOcrClient _glm = new GlmOcrClient();
|
||||
private readonly QianfanOcrClient _qianfan = new QianfanOcrClient();
|
||||
private readonly UapisOcrClient _uapis = new UapisOcrClient();
|
||||
private readonly UmiOcrClient _umi = new UmiOcrClient();
|
||||
|
||||
/// <summary>当前 OCR 引擎(baidu/glm/qianfan/uapis/umi)。</summary>
|
||||
public string Engine
|
||||
{
|
||||
get { return _engine; }
|
||||
}
|
||||
|
||||
// 内置默认 Key(Base64 混淆,exe 内非明文):未保存过配置/Key 为空时直接使用
|
||||
private static readonly string _defAk = Encoding.UTF8.GetString(Convert.FromBase64String("R0hjbjVxRmJLcG5TY3NyZHlIV1M4dzBB"));
|
||||
private static readonly string _defSk = Encoding.UTF8.GetString(Convert.FromBase64String("VkMxVVU2RXlxN28wcGRLNElCZ1A4ZnNhWjIyQ3RlMWM="));
|
||||
|
||||
public OcrClient()
|
||||
{
|
||||
_glm.StatusChanged += msg =>
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(msg);
|
||||
};
|
||||
_qianfan.StatusChanged += msg =>
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(msg);
|
||||
};
|
||||
_uapis.StatusChanged += msg =>
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(msg);
|
||||
};
|
||||
_umi.StatusChanged += msg =>
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(msg);
|
||||
};
|
||||
LoadConfig();
|
||||
}
|
||||
|
||||
/// <summary>读 baidu_ocr.json(OCR设置窗口保存后调用可热更新 Key/引擎)。</summary>
|
||||
public void LoadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
string p = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json");
|
||||
if (File.Exists(p))
|
||||
{
|
||||
var jsv = _json.Deserialize<Dictionary<string, string>>(File.ReadAllText(p));
|
||||
if (jsv != null)
|
||||
{
|
||||
_ak = jsv.ContainsKey("api_key") ? (jsv["api_key"] ?? "") : "";
|
||||
_sk = jsv.ContainsKey("secret_key") ? (jsv["secret_key"] ?? "") : "";
|
||||
_engine = jsv.ContainsKey("engine") ? (jsv["engine"] ?? "baidu") : "baidu";
|
||||
_glm.SetApiKey(jsv.ContainsKey("glm_api_key") ? (jsv["glm_api_key"] ?? "") : "");
|
||||
_qianfan.SetApiKey(jsv.ContainsKey("qianfan_api_key") ? (jsv["qianfan_api_key"] ?? "") : "");
|
||||
_uapis.SetApiKey(jsv.ContainsKey("uapis_api_key") ? (jsv["uapis_api_key"] ?? "") : "");
|
||||
_umi.SetBaseUrl(jsv.ContainsKey("umi_url") ? (jsv["umi_url"] ?? "") : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
// 缺省回退内置默认 Key:装完即用,无需打开设置点保存;用户保存过则文件值优先
|
||||
if (string.IsNullOrEmpty(_ak)) _ak = _defAk;
|
||||
if (string.IsNullOrEmpty(_sk)) _sk = _defSk;
|
||||
if (_engine != "baidu" && _engine != "glm" && _engine != "qianfan" && _engine != "uapis" && _engine != "umi")
|
||||
_engine = "baidu";
|
||||
}
|
||||
|
||||
public bool HasKey
|
||||
{
|
||||
get { return !string.IsNullOrEmpty(_ak) && !string.IsNullOrEmpty(_sk); }
|
||||
}
|
||||
|
||||
private void Notify(string msg)
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(msg);
|
||||
}
|
||||
|
||||
/// <summary>获取 access_token(内存→文件缓存→在线换取)。失败抛异常。</summary>
|
||||
private string GetToken()
|
||||
{
|
||||
if (_token != null && DateTime.Now < _tokenExpire) return _token;
|
||||
|
||||
string cachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_token.json");
|
||||
try
|
||||
{
|
||||
if (File.Exists(cachePath))
|
||||
{
|
||||
var c = _json.Deserialize<Dictionary<string, object>>(File.ReadAllText(cachePath));
|
||||
if (c != null && c.ContainsKey("token") && c.ContainsKey("expire"))
|
||||
{
|
||||
double exp;
|
||||
double.TryParse(Convert.ToString(c["expire"]), out exp);
|
||||
string tok = Convert.ToString(c["token"]);
|
||||
if (tok.Length > 0 && DateTime.Now < new DateTime(1970, 1, 1).AddSeconds(exp))
|
||||
{
|
||||
_token = tok;
|
||||
_tokenExpire = new DateTime(1970, 1, 1).AddSeconds(exp);
|
||||
return _token;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (!HasKey)
|
||||
throw new Exception("未配置百度 OCR Key(工具栏 OCR设置 填写 API Key / Secret Key)");
|
||||
|
||||
string url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
|
||||
+ "&client_id=" + Uri.EscapeDataString(_ak)
|
||||
+ "&client_secret=" + Uri.EscapeDataString(_sk);
|
||||
using (var wc = new WebClient())
|
||||
{
|
||||
wc.Encoding = Encoding.UTF8;
|
||||
wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
string body = wc.UploadString(url, "POST", "");
|
||||
var resp = _json.Deserialize<Dictionary<string, object>>(body);
|
||||
if (resp == null || !resp.ContainsKey("access_token"))
|
||||
throw new Exception("百度 token 获取失败: " +
|
||||
(resp != null && resp.ContainsKey("error_description")
|
||||
? Convert.ToString(resp["error_description"])
|
||||
: body.Substring(0, Math.Min(120, body.Length))));
|
||||
_token = Convert.ToString(resp["access_token"]);
|
||||
double expires;
|
||||
double.TryParse(Convert.ToString(resp.ContainsKey("expires_in") ? resp["expires_in"] : "2592000"), out expires);
|
||||
_tokenExpire = DateTime.Now.AddSeconds(Math.Max(3600, expires - 86400));
|
||||
try
|
||||
{
|
||||
var cache = new Dictionary<string, object>
|
||||
{
|
||||
{ "token", _token },
|
||||
{ "expire", (_tokenExpire - new DateTime(1970, 1, 1)).TotalSeconds }
|
||||
};
|
||||
File.WriteAllText(cachePath, _json.Serialize(cache));
|
||||
}
|
||||
catch { }
|
||||
return _token;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>识别位图(300DPI 裁剪)。失败抛异常;成功通知引擎来源。
|
||||
/// 竖排文字(实为旋转 90° 的横排)直接识别必乱码:细高框或平均置信度偏低时,
|
||||
/// 旋转 ±90° 重识别并按平均置信度选优,坐标逆向还原回原图。</summary>
|
||||
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
// ★ 引擎路由:智谱 GLM-OCR / 千帆 PaddleOCR-VL(版面解析,不走旋转重试)
|
||||
if (_engine == "glm")
|
||||
return _glm.RecognizeAsync(crop, timeoutMs);
|
||||
if (_engine == "qianfan")
|
||||
return _qianfan.RecognizeAsync(crop, timeoutMs);
|
||||
if (_engine == "uapis")
|
||||
return _uapis.RecognizeAsync(crop, timeoutMs);
|
||||
if (_engine == "umi")
|
||||
return _umi.RecognizeAsync(crop, timeoutMs);
|
||||
|
||||
var tcs = new TaskCompletionSource<List<OcrItem>>();
|
||||
Task.Factory.StartNew(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = RecognizeOnce(crop, timeoutMs);
|
||||
try
|
||||
{
|
||||
if (crop.Height >= crop.Width * 1.15f || AvgScore(items) < 0.88f)
|
||||
{
|
||||
float best = AvgScore(items);
|
||||
foreach (bool cw in new bool[] { true, false })
|
||||
{
|
||||
using (var turned = new Bitmap(crop))
|
||||
{
|
||||
turned.RotateFlip(cw ? RotateFlipType.Rotate90FlipNone : RotateFlipType.Rotate270FlipNone);
|
||||
var rr = RecognizeOnce(turned, timeoutMs);
|
||||
UnrotateItems(rr, cw, crop.Width, crop.Height);
|
||||
float s = AvgScore(rr);
|
||||
if (s > best + 0.01f) { best = s; items = rr; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* 旋转重试失败:保留原方向结果 */ }
|
||||
Notify("百度OCR·高精度含位置版");
|
||||
tcs.TrySetResult(items);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>单方向识别一次(编码→上传→解析);Box 为入参图像的像素坐标。</summary>
|
||||
private List<OcrItem> RecognizeOnce(Bitmap img, int timeoutMs)
|
||||
{
|
||||
// 编码为百度限额内(高精度版 base64 后 ≤4MB)的 JPEG;返回缩放比用于还原识别坐标
|
||||
float sendScale;
|
||||
string b64 = EncodeFit(img, out sendScale);
|
||||
string tok = GetToken();
|
||||
string url = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate?access_token="
|
||||
+ Uri.EscapeDataString(tok);
|
||||
// Base64 的表单转义不能用 Uri.EscapeDataString:.NET Framework 对超长字符串
|
||||
// (约 32K 字符)抛"无效的 URI: URI 字符串太长",大图 base64 必中
|
||||
string form = "image=" + EscapeBase64(b64)
|
||||
+ "&language_type=CHN_ENG&probability=true";
|
||||
using (var wc = new WebClient())
|
||||
{
|
||||
wc.Encoding = Encoding.UTF8;
|
||||
wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
string body = wc.UploadString(url, "POST", form);
|
||||
var resp = _json.Deserialize<Dictionary<string, object>>(body);
|
||||
if (resp == null)
|
||||
throw new Exception("百度OCR返回解析失败");
|
||||
if (resp.ContainsKey("error_code") && Convert.ToInt64(resp["error_code"]) != 0)
|
||||
throw new Exception("百度OCR错误 " + Convert.ToString(resp["error_code"]) + ": "
|
||||
+ Convert.ToString(resp.ContainsKey("error_msg") ? resp["error_msg"] : ""));
|
||||
var items = new List<OcrItem>();
|
||||
var arr = resp.ContainsKey("words_result") ? resp["words_result"] as System.Collections.ArrayList : null;
|
||||
if (arr != null)
|
||||
{
|
||||
foreach (Dictionary<string, object> w in arr)
|
||||
{
|
||||
var loc = (w != null && w.ContainsKey("location")) ? w["location"] as Dictionary<string, object> : null;
|
||||
if (loc == null) continue;
|
||||
float l = LocF(loc, "left"), t = LocF(loc, "top");
|
||||
float w2 = LocF(loc, "width"), h = LocF(loc, "height");
|
||||
float score = 0.95f;
|
||||
var prob = w.ContainsKey("probability") ? w["probability"] as Dictionary<string, object> : null;
|
||||
if (prob != null && prob.ContainsKey("average"))
|
||||
{
|
||||
float sv;
|
||||
if (float.TryParse(Convert.ToString(prob["average"]), out sv)) score = sv;
|
||||
}
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = Convert.ToString(w.ContainsKey("words") ? w["words"] : ""),
|
||||
// location 是发送图的像素坐标,除以缩放比还原到原始裁剪图坐标
|
||||
Box = new PointF[]
|
||||
{
|
||||
new PointF(l / sendScale, t / sendScale),
|
||||
new PointF((l + w2) / sendScale, t / sendScale),
|
||||
new PointF((l + w2) / sendScale, (t + h) / sendScale),
|
||||
new PointF(l / sendScale, (t + h) / sendScale)
|
||||
},
|
||||
Score = score
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>平均置信度(无结果时 -1,便于选优时落败)。</summary>
|
||||
private static float AvgScore(List<OcrItem> items)
|
||||
{
|
||||
if (items == null || items.Count == 0) return -1f;
|
||||
float sum = 0;
|
||||
foreach (var it in items) sum += it.Score;
|
||||
return sum / items.Count;
|
||||
}
|
||||
|
||||
/// <summary>把顺/逆时针旋转 90° 后识别出的 Box 坐标逆向还原到原图坐标系
|
||||
/// (还原后重排为 左上/右上/右下/左下,Bounds 语义不变)。</summary>
|
||||
private static void UnrotateItems(List<OcrItem> items, bool clockwise, int w, int h)
|
||||
{
|
||||
foreach (var it in items)
|
||||
{
|
||||
if (it.Box == null) continue;
|
||||
float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;
|
||||
foreach (var p in it.Box)
|
||||
{
|
||||
float x, y;
|
||||
if (clockwise) { x = p.Y; y = h - 1f - p.X; }
|
||||
else { x = w - 1f - p.Y; y = p.X; }
|
||||
if (x < l) l = x; if (x > r) r = x;
|
||||
if (y < t) t = y; if (y > b) b = y;
|
||||
}
|
||||
it.Box = new PointF[]
|
||||
{
|
||||
new PointF(l, t), new PointF(r, t),
|
||||
new PointF(r, b), new PointF(l, b)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static float LocF(Dictionary<string, object> loc, string key)
|
||||
{
|
||||
float v;
|
||||
return (loc != null && loc.ContainsKey(key) &&
|
||||
float.TryParse(Convert.ToString(loc[key]), out v)) ? v : 0f;
|
||||
}
|
||||
|
||||
/// <summary>编码为百度限额内的 JPEG(高精度版要求 base64 后 ≤4MB):
|
||||
/// 质量 90/75/60 递降试编码,均超限则按 0.8 倍降采样重试(最多 8 轮,必然收敛)。
|
||||
/// scale=发送图/原图(≤1),调用方用它把返回坐标还原到原图。</summary>
|
||||
private static string EncodeFit(Bitmap img, out float scale)
|
||||
{
|
||||
scale = 1f;
|
||||
string last = null;
|
||||
for (int round = 0; round < 8; round++)
|
||||
{
|
||||
int w = Math.Max(1, (int)Math.Round(img.Width * scale));
|
||||
int h = Math.Max(1, (int)Math.Round(img.Height * scale));
|
||||
using (Bitmap cur = (w == img.Width && h == img.Height) ? new Bitmap(img) : new Bitmap(img, w, h))
|
||||
{
|
||||
foreach (long q in new long[] { 90, 75, 60 })
|
||||
{
|
||||
last = EncodeJpeg(cur, q);
|
||||
// base64 字符数≈UTF-8 字节数;3.9M 留余量防 4M 硬限被卡
|
||||
if (last.Length <= 3900000) return last;
|
||||
}
|
||||
}
|
||||
scale *= 0.8f;
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
private static string EncodeJpeg(Bitmap bmp, long quality)
|
||||
{
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
var codec = GetJpegEncoder();
|
||||
if (codec != null)
|
||||
{
|
||||
using (var ep = new EncoderParameters(1))
|
||||
{
|
||||
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
|
||||
bmp.Save(ms, codec, ep);
|
||||
}
|
||||
}
|
||||
else bmp.Save(ms, ImageFormat.Jpeg);
|
||||
return Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Base64 的 URI 表单转义:仅 + / = 三个字符转 %XX,字母数字无需转义。
|
||||
/// 不能用 Uri.EscapeDataString:.NET Framework 对约 32K 以上字符串抛"URI 字符串太长"。</summary>
|
||||
private static string EscapeBase64(string s)
|
||||
{
|
||||
var sb = new StringBuilder(s.Length + s.Length / 2);
|
||||
for (int i = 0; i < s.Length; i++)
|
||||
{
|
||||
char c = s[i];
|
||||
if (c == '+') sb.Append("%2B");
|
||||
else if (c == '/') sb.Append("%2F");
|
||||
else if (c == '=') sb.Append("%3D");
|
||||
else sb.Append(c);
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static ImageCodecInfo GetJpegEncoder()
|
||||
{
|
||||
foreach (var c in ImageCodecInfo.GetImageEncoders())
|
||||
if (c.FormatID == ImageFormat.Jpeg.Guid) return c;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
Generated
+303
@@ -0,0 +1,303 @@
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>OCR 设置对话框:引擎选择(百度/智谱GLM) + 对应 Key(存 exe 旁 baidu_ocr.json)。</summary>
|
||||
public partial class OcrSettingsForm : System.Windows.Forms.Form
|
||||
{
|
||||
/// <summary>必需的设计器变量。</summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>清理正在使用的资源。</summary>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lblEngine = new System.Windows.Forms.Label();
|
||||
this.cboEngine = new System.Windows.Forms.ComboBox();
|
||||
this.lblApiKey = new System.Windows.Forms.Label();
|
||||
this.txtApiKey = new System.Windows.Forms.TextBox();
|
||||
this.lblSecretKey = new System.Windows.Forms.Label();
|
||||
this.txtSecretKey = new System.Windows.Forms.TextBox();
|
||||
this.lblGlmKey = new System.Windows.Forms.Label();
|
||||
this.txtGlmKey = new System.Windows.Forms.TextBox();
|
||||
this.lblQianfanKey = new System.Windows.Forms.Label();
|
||||
this.txtQianfanKey = new System.Windows.Forms.TextBox();
|
||||
this.lblUapisKey = new System.Windows.Forms.Label();
|
||||
this.txtUapisKey = new System.Windows.Forms.TextBox();
|
||||
this.lblUmiUrl = new System.Windows.Forms.Label();
|
||||
this.txtUmiUrl = new System.Windows.Forms.TextBox();
|
||||
this.btnUmi下载 = new System.Windows.Forms.Button();
|
||||
this.btnUmi检测 = new System.Windows.Forms.Button();
|
||||
this.lblUmi状态 = new System.Windows.Forms.Label();
|
||||
this.btn帮助 = new System.Windows.Forms.Button();
|
||||
this.lbl说明 = new System.Windows.Forms.Label();
|
||||
this.btn确定 = new System.Windows.Forms.Button();
|
||||
this.btn取消 = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lblEngine
|
||||
//
|
||||
this.lblEngine.AutoSize = true;
|
||||
this.lblEngine.Location = new System.Drawing.Point(12, 18);
|
||||
this.lblEngine.Name = "lblEngine";
|
||||
this.lblEngine.Size = new System.Drawing.Size(89, 18);
|
||||
this.lblEngine.TabIndex = 0;
|
||||
this.lblEngine.Text = "识别引擎:";
|
||||
//
|
||||
// cboEngine
|
||||
//
|
||||
this.cboEngine.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
|
||||
this.cboEngine.Items.AddRange(new object[] {
|
||||
"百度 OCR 高精度(含位置)",
|
||||
"智谱 GLM-OCR(版面解析/手写强)",
|
||||
"百度 PaddleOCR-VL(千帆版面解析)",
|
||||
"UApiPro 通用OCR(免费积分/带位置)",
|
||||
"Umi-OCR 本地引擎(离线免费,需先运行Umi-OCR)"});
|
||||
this.cboEngine.Location = new System.Drawing.Point(139, 15);
|
||||
this.cboEngine.Name = "cboEngine";
|
||||
this.cboEngine.Size = new System.Drawing.Size(389, 26);
|
||||
this.cboEngine.TabIndex = 1;
|
||||
this.cboEngine.SelectedIndexChanged += new System.EventHandler(this.cboEngine_SelectedIndexChanged);
|
||||
//
|
||||
// lblApiKey
|
||||
//
|
||||
this.lblApiKey.AutoSize = true;
|
||||
this.lblApiKey.Location = new System.Drawing.Point(12, 55);
|
||||
this.lblApiKey.Name = "lblApiKey";
|
||||
this.lblApiKey.Size = new System.Drawing.Size(80, 18);
|
||||
this.lblApiKey.TabIndex = 2;
|
||||
this.lblApiKey.Text = "API Key:";
|
||||
//
|
||||
// txtApiKey
|
||||
//
|
||||
this.txtApiKey.Location = new System.Drawing.Point(139, 52);
|
||||
this.txtApiKey.Name = "txtApiKey";
|
||||
this.txtApiKey.Size = new System.Drawing.Size(389, 28);
|
||||
this.txtApiKey.TabIndex = 3;
|
||||
this.txtApiKey.Text = "GHcn5qFbKpnScsrdyHWS8w0A";
|
||||
this.txtApiKey.UseSystemPasswordChar = true;
|
||||
//
|
||||
// lblSecretKey
|
||||
//
|
||||
this.lblSecretKey.AutoSize = true;
|
||||
this.lblSecretKey.Location = new System.Drawing.Point(12, 89);
|
||||
this.lblSecretKey.Name = "lblSecretKey";
|
||||
this.lblSecretKey.Size = new System.Drawing.Size(107, 18);
|
||||
this.lblSecretKey.TabIndex = 4;
|
||||
this.lblSecretKey.Text = "Secret Key:";
|
||||
//
|
||||
// txtSecretKey
|
||||
//
|
||||
this.txtSecretKey.Location = new System.Drawing.Point(139, 89);
|
||||
this.txtSecretKey.Name = "txtSecretKey";
|
||||
this.txtSecretKey.Size = new System.Drawing.Size(389, 28);
|
||||
this.txtSecretKey.TabIndex = 5;
|
||||
this.txtSecretKey.Text = "VC1UU6Eyq7o0pdK4IBgP8fsaZ22Cte1c";
|
||||
this.txtSecretKey.UseSystemPasswordChar = true;
|
||||
//
|
||||
// lblGlmKey
|
||||
//
|
||||
this.lblGlmKey.AutoSize = true;
|
||||
this.lblGlmKey.Location = new System.Drawing.Point(12, 55);
|
||||
this.lblGlmKey.Name = "lblGlmKey";
|
||||
this.lblGlmKey.Size = new System.Drawing.Size(125, 18);
|
||||
this.lblGlmKey.TabIndex = 2;
|
||||
this.lblGlmKey.Text = "智谱 API Key:";
|
||||
//
|
||||
// txtGlmKey
|
||||
//
|
||||
this.txtGlmKey.Location = new System.Drawing.Point(139, 52);
|
||||
this.txtGlmKey.Name = "txtGlmKey";
|
||||
this.txtGlmKey.Size = new System.Drawing.Size(389, 28);
|
||||
this.txtGlmKey.TabIndex = 3;
|
||||
this.txtGlmKey.UseSystemPasswordChar = true;
|
||||
//
|
||||
// lblQianfanKey
|
||||
//
|
||||
this.lblQianfanKey.AutoSize = true;
|
||||
this.lblQianfanKey.Location = new System.Drawing.Point(12, 55);
|
||||
this.lblQianfanKey.Name = "lblQianfanKey";
|
||||
this.lblQianfanKey.Size = new System.Drawing.Size(125, 18);
|
||||
this.lblQianfanKey.TabIndex = 2;
|
||||
this.lblQianfanKey.Text = "千帆 API Key:";
|
||||
//
|
||||
// txtQianfanKey
|
||||
//
|
||||
this.txtQianfanKey.Location = new System.Drawing.Point(139, 52);
|
||||
this.txtQianfanKey.Name = "txtQianfanKey";
|
||||
this.txtQianfanKey.Size = new System.Drawing.Size(389, 28);
|
||||
this.txtQianfanKey.TabIndex = 3;
|
||||
this.txtQianfanKey.UseSystemPasswordChar = true;
|
||||
//
|
||||
// lblUapisKey
|
||||
//
|
||||
this.lblUapisKey.AutoSize = true;
|
||||
this.lblUapisKey.Location = new System.Drawing.Point(12, 55);
|
||||
this.lblUapisKey.Name = "lblUapisKey";
|
||||
this.lblUapisKey.Size = new System.Drawing.Size(116, 18);
|
||||
this.lblUapisKey.TabIndex = 2;
|
||||
this.lblUapisKey.Text = "UApiPro Key:";
|
||||
//
|
||||
// txtUapisKey
|
||||
//
|
||||
this.txtUapisKey.Location = new System.Drawing.Point(139, 52);
|
||||
this.txtUapisKey.Name = "txtUapisKey";
|
||||
this.txtUapisKey.Size = new System.Drawing.Size(389, 28);
|
||||
this.txtUapisKey.TabIndex = 3;
|
||||
this.txtUapisKey.UseSystemPasswordChar = true;
|
||||
//
|
||||
// lblUmiUrl
|
||||
//
|
||||
this.lblUmiUrl.AutoSize = true;
|
||||
this.lblUmiUrl.Location = new System.Drawing.Point(12, 55);
|
||||
this.lblUmiUrl.Name = "lblUmiUrl";
|
||||
this.lblUmiUrl.Size = new System.Drawing.Size(89, 18);
|
||||
this.lblUmiUrl.TabIndex = 2;
|
||||
this.lblUmiUrl.Text = "服务地址:";
|
||||
//
|
||||
// txtUmiUrl
|
||||
//
|
||||
this.txtUmiUrl.Location = new System.Drawing.Point(139, 52);
|
||||
this.txtUmiUrl.Name = "txtUmiUrl";
|
||||
this.txtUmiUrl.Size = new System.Drawing.Size(389, 28);
|
||||
this.txtUmiUrl.TabIndex = 3;
|
||||
this.txtUmiUrl.Text = "http://127.0.0.1:1224";
|
||||
//
|
||||
// btnUmi下载
|
||||
//
|
||||
this.btnUmi下载.Location = new System.Drawing.Point(139, 86);
|
||||
this.btnUmi下载.Name = "btnUmi下载";
|
||||
this.btnUmi下载.Size = new System.Drawing.Size(139, 28);
|
||||
this.btnUmi下载.TabIndex = 4;
|
||||
this.btnUmi下载.Text = "下载 OCR 本体";
|
||||
this.btnUmi下载.UseVisualStyleBackColor = true;
|
||||
this.btnUmi下载.Click += new System.EventHandler(this.btnUmi下载_Click);
|
||||
//
|
||||
// btnUmi检测
|
||||
//
|
||||
this.btnUmi检测.Location = new System.Drawing.Point(304, 86);
|
||||
this.btnUmi检测.Name = "btnUmi检测";
|
||||
this.btnUmi检测.Size = new System.Drawing.Size(138, 28);
|
||||
this.btnUmi检测.TabIndex = 5;
|
||||
this.btnUmi检测.Text = "检测 OCR 本体";
|
||||
this.btnUmi检测.UseVisualStyleBackColor = true;
|
||||
this.btnUmi检测.Click += new System.EventHandler(this.btnUmi检测_Click);
|
||||
//
|
||||
// lblUmi状态
|
||||
//
|
||||
this.lblUmi状态.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
this.lblUmi状态.Location = new System.Drawing.Point(12, 118);
|
||||
this.lblUmi状态.Name = "lblUmi状态";
|
||||
this.lblUmi状态.Size = new System.Drawing.Size(506, 20);
|
||||
this.lblUmi状态.TabIndex = 6;
|
||||
//
|
||||
// btn帮助
|
||||
//
|
||||
this.btn帮助.Location = new System.Drawing.Point(12, 200);
|
||||
this.btn帮助.Name = "btn帮助";
|
||||
this.btn帮助.Size = new System.Drawing.Size(90, 28);
|
||||
this.btn帮助.TabIndex = 10;
|
||||
this.btn帮助.Text = "帮助";
|
||||
this.btn帮助.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lbl说明
|
||||
//
|
||||
this.lbl说明.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
this.lbl说明.Location = new System.Drawing.Point(12, 142);
|
||||
this.lbl说明.Name = "lbl说明";
|
||||
this.lbl说明.Size = new System.Drawing.Size(506, 46);
|
||||
this.lbl说明.TabIndex = 7;
|
||||
this.lbl说明.Text = "百度引擎已内置默认 Key,可直接使用;换自己的 Key 更稳。\r\n智谱 GLM-OCR(手写字/版面强)需到 open.bigmodel.cn 获取 API K" +
|
||||
"ey,约 0.2 元/百万 tokens。\r\nUmi-OCR 为本机离线引擎(免费无限次):本体将从网盘自动下载到程序目录,识别时自动启动、关窗自动关闭。";
|
||||
//
|
||||
// btn确定
|
||||
//
|
||||
this.btn确定.DialogResult = System.Windows.Forms.DialogResult.OK;
|
||||
this.btn确定.Location = new System.Drawing.Point(367, 200);
|
||||
this.btn确定.Name = "btn确定";
|
||||
this.btn确定.Size = new System.Drawing.Size(75, 28);
|
||||
this.btn确定.TabIndex = 8;
|
||||
this.btn确定.Text = "保存";
|
||||
this.btn确定.UseVisualStyleBackColor = true;
|
||||
this.btn确定.Click += new System.EventHandler(this.btn确定_Click);
|
||||
//
|
||||
// btn取消
|
||||
//
|
||||
this.btn取消.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.btn取消.Location = new System.Drawing.Point(449, 200);
|
||||
this.btn取消.Name = "btn取消";
|
||||
this.btn取消.Size = new System.Drawing.Size(75, 28);
|
||||
this.btn取消.TabIndex = 9;
|
||||
this.btn取消.Text = "取消";
|
||||
this.btn取消.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// OcrSettingsForm
|
||||
//
|
||||
this.AcceptButton = this.btn确定;
|
||||
this.CancelButton = this.btn取消;
|
||||
this.ClientSize = new System.Drawing.Size(539, 244);
|
||||
this.Controls.Add(this.lblEngine);
|
||||
this.Controls.Add(this.cboEngine);
|
||||
this.Controls.Add(this.lblApiKey);
|
||||
this.Controls.Add(this.txtApiKey);
|
||||
this.Controls.Add(this.lblSecretKey);
|
||||
this.Controls.Add(this.txtSecretKey);
|
||||
this.Controls.Add(this.lblGlmKey);
|
||||
this.Controls.Add(this.txtGlmKey);
|
||||
this.Controls.Add(this.lblQianfanKey);
|
||||
this.Controls.Add(this.txtQianfanKey);
|
||||
this.Controls.Add(this.lblUapisKey);
|
||||
this.Controls.Add(this.txtUapisKey);
|
||||
this.Controls.Add(this.lblUmiUrl);
|
||||
this.Controls.Add(this.txtUmiUrl);
|
||||
this.Controls.Add(this.btnUmi下载);
|
||||
this.Controls.Add(this.btnUmi检测);
|
||||
this.Controls.Add(this.lblUmi状态);
|
||||
this.Controls.Add(this.lbl说明);
|
||||
this.Controls.Add(this.btn帮助);
|
||||
this.Controls.Add(this.btn确定);
|
||||
this.Controls.Add(this.btn取消);
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "OcrSettingsForm";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||
this.Text = "OCR 设置";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label lblEngine;
|
||||
private System.Windows.Forms.ComboBox cboEngine;
|
||||
private System.Windows.Forms.Label lblApiKey;
|
||||
private System.Windows.Forms.TextBox txtApiKey;
|
||||
private System.Windows.Forms.Label lblSecretKey;
|
||||
private System.Windows.Forms.TextBox txtSecretKey;
|
||||
private System.Windows.Forms.Label lblGlmKey;
|
||||
private System.Windows.Forms.TextBox txtGlmKey;
|
||||
private System.Windows.Forms.Label lblQianfanKey;
|
||||
private System.Windows.Forms.TextBox txtQianfanKey;
|
||||
private System.Windows.Forms.Label lblUapisKey;
|
||||
private System.Windows.Forms.TextBox txtUapisKey;
|
||||
private System.Windows.Forms.Label lblUmiUrl;
|
||||
private System.Windows.Forms.TextBox txtUmiUrl;
|
||||
private System.Windows.Forms.Button btnUmi下载;
|
||||
private System.Windows.Forms.Button btnUmi检测;
|
||||
private System.Windows.Forms.Label lblUmi状态;
|
||||
private System.Windows.Forms.Button btn帮助;
|
||||
private System.Windows.Forms.Label lbl说明;
|
||||
private System.Windows.Forms.Button btn确定;
|
||||
private System.Windows.Forms.Button btn取消;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
// OcrSettingsForm:OCR 引擎与 Key 设置(百度 OCR / 智谱 GLM-OCR / 千帆 / UApiPro / Umi-OCR 本地)
|
||||
// 保存 → exe 旁 baidu_ocr.json(engine/api_key/secret_key/glm_api_key/qianfan_api_key/uapis_api_key/umi_url)
|
||||
// + 热更新 OcrClient + 作废百度 token 缓存。
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
public partial class OcrSettingsForm : Form
|
||||
{
|
||||
private readonly OcrClient _ocr;
|
||||
|
||||
public OcrSettingsForm(OcrClient ocr)
|
||||
{
|
||||
InitializeComponent();
|
||||
_ocr = ocr;
|
||||
|
||||
// 回填现有配置(默认 Key 在 Designer 的 Text 里,密码遮罩显示;保存过则文件值优先)
|
||||
string engine = "baidu";
|
||||
try
|
||||
{
|
||||
string cfgPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json");
|
||||
if (File.Exists(cfgPath))
|
||||
{
|
||||
var jsv = new JavaScriptSerializer().Deserialize<Dictionary<string, string>>(File.ReadAllText(cfgPath));
|
||||
if (jsv != null)
|
||||
{
|
||||
if (jsv.ContainsKey("engine")) engine = jsv["engine"] ?? "baidu";
|
||||
txtApiKey.Text = jsv.ContainsKey("api_key") ? (jsv["api_key"] ?? "") : "";
|
||||
txtSecretKey.Text = jsv.ContainsKey("secret_key") ? (jsv["secret_key"] ?? "") : "";
|
||||
txtGlmKey.Text = jsv.ContainsKey("glm_api_key") ? (jsv["glm_api_key"] ?? "") : "";
|
||||
txtQianfanKey.Text = jsv.ContainsKey("qianfan_api_key") ? (jsv["qianfan_api_key"] ?? "") : "";
|
||||
txtUapisKey.Text = jsv.ContainsKey("uapis_api_key") ? (jsv["uapis_api_key"] ?? "") : "";
|
||||
txtUmiUrl.Text = jsv.ContainsKey("umi_url") && !string.IsNullOrWhiteSpace(jsv["umi_url"])
|
||||
? jsv["umi_url"] : "http://127.0.0.1:1224";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
cboEngine.SelectedIndex =
|
||||
engine == "glm" ? 1 :
|
||||
engine == "qianfan" ? 2 :
|
||||
engine == "uapis" ? 3 :
|
||||
engine == "umi" ? 4 : 0;
|
||||
UpdateEnginePanels();
|
||||
}
|
||||
|
||||
/// <summary>按引擎下拉显示对应的输入区(百度=两格;智谱/千帆/UApiPro=Key一格;Umi-OCR=服务地址一格)。</summary>
|
||||
private void UpdateEnginePanels()
|
||||
{
|
||||
int sel = cboEngine.SelectedIndex;
|
||||
bool glm = sel == 1;
|
||||
bool qf = sel == 2;
|
||||
bool ua = sel == 3;
|
||||
bool umi = sel == 4;
|
||||
bool baidu = !glm && !qf && !ua && !umi;
|
||||
lblApiKey.Visible = baidu;
|
||||
txtApiKey.Visible = baidu;
|
||||
lblSecretKey.Visible = baidu;
|
||||
txtSecretKey.Visible = baidu;
|
||||
lblGlmKey.Visible = glm;
|
||||
txtGlmKey.Visible = glm;
|
||||
lblQianfanKey.Visible = qf;
|
||||
txtQianfanKey.Visible = qf;
|
||||
lblUapisKey.Visible = ua;
|
||||
txtUapisKey.Visible = ua;
|
||||
lblUmiUrl.Visible = umi;
|
||||
txtUmiUrl.Visible = umi;
|
||||
btnUmi下载.Visible = umi;
|
||||
btnUmi检测.Visible = umi;
|
||||
lblUmi状态.Visible = umi;
|
||||
if (umi) RefreshUmiStatus();
|
||||
}
|
||||
|
||||
/// <summary>刷新 Umi 本体三态状态显示。</summary>
|
||||
private void RefreshUmiStatus()
|
||||
{
|
||||
try { lblUmi状态.Text = UmiOcrHost.DetectText(); } catch { }
|
||||
}
|
||||
|
||||
/// <summary>检测 OCR 本体:刷新状态(未安装/已安装未运行/运行中)。</summary>
|
||||
private void btnUmi检测_Click(object sender, EventArgs e)
|
||||
{
|
||||
RefreshUmiStatus();
|
||||
MessageBox.Show(this, UmiOcrHost.DetectText(), "检测 OCR 本体",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
/// <summary>下载 OCR 本体:网盘直链(~179MB)→解压到程序目录 Umi-OCR\→验证→删 zip。</summary>
|
||||
private void btnUmi下载_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (UmiOcrHost.IsInstalled)
|
||||
{
|
||||
var r = MessageBox.Show(this, "OCR 本体已存在,重新下载将覆盖安装。继续吗?", "下载 OCR 本体",
|
||||
MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
|
||||
if (r != DialogResult.OK) return;
|
||||
}
|
||||
btnUmi下载.Enabled = false;
|
||||
btnUmi检测.Enabled = false;
|
||||
lblUmi状态.ForeColor = System.Drawing.Color.DimGray;
|
||||
lblUmi状态.Text = "正在下载… 0%";
|
||||
int lastPct = -1;
|
||||
UmiOcrHost.DownloadAndInstallAsync(pct =>
|
||||
{
|
||||
if (pct == lastPct) return;
|
||||
lastPct = pct;
|
||||
try { this.BeginInvoke((MethodInvoker)delegate
|
||||
{
|
||||
lblUmi状态.Text = pct >= 100 ? "下载完成,正在解压安装…" : "正在下载… " + pct + "%";
|
||||
}); } catch { }
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
try { this.BeginInvoke((MethodInvoker)delegate
|
||||
{
|
||||
btnUmi下载.Enabled = true;
|
||||
btnUmi检测.Enabled = true;
|
||||
lblUmi状态.ForeColor = System.Drawing.SystemColors.GrayText;
|
||||
if (t.IsFaulted)
|
||||
{
|
||||
Exception root = t.Exception;
|
||||
while (root is AggregateException && root.InnerException != null) root = root.InnerException;
|
||||
lblUmi状态.Text = "下载/安装失败: " + root.Message;
|
||||
MessageBox.Show(this, "下载/安装失败:\n" + root.Message, "下载 OCR 本体",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshUmiStatus();
|
||||
MessageBox.Show(this, "OCR 本体安装完成!\n" + UmiOcrHost.DetectText() +
|
||||
"\n\n选 Umi-OCR 引擎保存后即可使用(识别时自动启动)。",
|
||||
"下载 OCR 本体", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}); } catch { }
|
||||
});
|
||||
}
|
||||
|
||||
private void cboEngine_SelectedIndexChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateEnginePanels();
|
||||
}
|
||||
|
||||
/// <summary>帮助:全部引擎的 Key 获取方式与本地 OCR 设置流程。</summary>
|
||||
private void btn帮助_Click(object sender, EventArgs e)
|
||||
{
|
||||
string help =
|
||||
"════════ OCR 识别引擎使用帮助 ════════\r\n" +
|
||||
"\r\n" +
|
||||
"【1. 百度 OCR 高精度(含位置)】 默认引擎,已内置 Key 可直接用\r\n" +
|
||||
" · 获取自己的 Key(更稳,免费额度足够日常):\r\n" +
|
||||
" ① 打开 console.baidu.com 登录百度账号\r\n" +
|
||||
" ② 进入\"产品服务 / 文字识别\"→ 创建应用\r\n" +
|
||||
" ③ 勾选\"文字识别\"全部接口,创建完成\r\n" +
|
||||
" ④ 在应用详情里复制 API Key 和 Secret Key 填入本窗口\r\n" +
|
||||
"\r\n" +
|
||||
"【2. 智谱 GLM-OCR(版面解析/手写强)】\r\n" +
|
||||
" · 到 open.bigmodel.cn 注册并实名 → API Keys 页面新建密钥\r\n" +
|
||||
" · 把密钥填入\"智谱 API Key\"\r\n" +
|
||||
" · 计费约 0.2 元/百万 tokens,适合手写体/复杂版面\r\n" +
|
||||
"\r\n" +
|
||||
"【3. 百度 PaddleOCR-VL(千帆版面解析)】\r\n" +
|
||||
" · 到 console.bce.baidu.com → 千帆 ModelBuilder → 应用管理\r\n" +
|
||||
" · 创建应用后复制 API Key 填入\"千帆 API Key\"\r\n" +
|
||||
" · 需在千帆控制台开通 PaddleOCR-VL 模型服务\r\n" +
|
||||
"\r\n" +
|
||||
"【4. UApiPro 通用OCR(免费积分/带位置)】\r\n" +
|
||||
" · 到 uapis.cn 注册账号 → 控制台 → API Keys → 新建\r\n" +
|
||||
" · 把 Key 填入\"UApiPro Key\"\r\n" +
|
||||
" · 注册送免费积分(约 4 积分/次识别),小量使用零成本\r\n" +
|
||||
"\r\n" +
|
||||
"【5. Umi-OCR 本地引擎(离线免费,无限次)】\r\n" +
|
||||
" · 本地引擎无需任何 Key,识别全在本机完成,数据不出电脑\r\n" +
|
||||
" · 设置流程:\r\n" +
|
||||
" ① 选本引擎后点\"下载 OCR 本体\"(约 179MB,自动下载安装)\r\n" +
|
||||
" ② 点\"检测 OCR 本体\"确认状态为\"运行中\"或\"已安装\"\r\n" +
|
||||
" ③ 点\"保存\"完成,之后识别时自动启动、关窗自动关闭\r\n" +
|
||||
" · 服务地址保持默认 http://127.0.0.1:1224 即可\r\n" +
|
||||
" (只有你在 Umi-OCR 软件里改过 HTTP 端口才需要改这里)\r\n" +
|
||||
"\r\n" +
|
||||
"════════════════════════════════\r\n" +
|
||||
"提示: Key 全部以密码方式显示,点\"保存\"后立即生效;\r\n" +
|
||||
"所有 Key 统一保存在程序目录 baidu_ocr.json。";
|
||||
MessageBox.Show(this, help, "OCR 使用帮助",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
private void btn确定_Click(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
string cfgPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_ocr.json");
|
||||
string engineValue =
|
||||
cboEngine.SelectedIndex == 1 ? "glm" :
|
||||
cboEngine.SelectedIndex == 2 ? "qianfan" :
|
||||
cboEngine.SelectedIndex == 3 ? "uapis" :
|
||||
cboEngine.SelectedIndex == 4 ? "umi" : "baidu";
|
||||
var jsv = new JavaScriptSerializer();
|
||||
File.WriteAllText(cfgPath, jsv.Serialize(new Dictionary<string, string>
|
||||
{
|
||||
{ "engine", engineValue },
|
||||
{ "api_key", txtApiKey.Text.Trim() },
|
||||
{ "secret_key", txtSecretKey.Text.Trim() },
|
||||
{ "glm_api_key", txtGlmKey.Text.Trim() },
|
||||
{ "qianfan_api_key", txtQianfanKey.Text.Trim() },
|
||||
{ "uapis_api_key", txtUapisKey.Text.Trim() },
|
||||
{ "umi_url", string.IsNullOrWhiteSpace(txtUmiUrl.Text.Trim()) ? "http://127.0.0.1:1224" : txtUmiUrl.Text.Trim() }
|
||||
}));
|
||||
// 热更新引擎/Key + 作废百度 token 缓存(下次识别重新换取)
|
||||
if (_ocr != null)
|
||||
{
|
||||
_ocr.LoadConfig();
|
||||
try { File.Delete(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "baidu_token.json")); } catch { }
|
||||
}
|
||||
// 选了 Umi-OCR 但还没装本体:提示引导下载(不阻断保存)
|
||||
if (engineValue == "umi" && !UmiOcrHost.IsInstalled)
|
||||
{
|
||||
MessageBox.Show(this, "Umi-OCR 本体尚未安装。\n请点\"下载 OCR 本体\"完成安装后即可使用。",
|
||||
"OCR 设置", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, "保存失败: " + ex.Message, "OCR 设置",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,148 @@
|
||||
// ============================================================
|
||||
// PdfBridge:报价插件(Form3)与 PDF 查看器之间的桥
|
||||
// 与 cadbt 的 CadBridge 保持相同的 API 形状:
|
||||
// · 下行(Form3→查看器):SendPaCommand / ZoomToFrameHandle /
|
||||
// DeleteFramesByHandles / MergeMasks / ClearHighlight
|
||||
// · 上行(查看器→Form3):PaPicked / EntityErased / PaExited /
|
||||
// F1/F2/F3Requested
|
||||
// 扒图逻辑:用户在 PDF 上框选 → 裁剪 → OCR 工作进程识别 →
|
||||
// 按 CAD 版语义组装 results(正向逐条/反向合并) → PaPicked
|
||||
// → Form3 入表格;查看器在框选位置画彩色蒙版框。
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
public static class PdfBridge
|
||||
{
|
||||
private static PdfViewerWindow _viewer;
|
||||
|
||||
/// <summary>PDF 查看器是否已挂载且打开了文档。</summary>
|
||||
public static bool IsViewerReady
|
||||
{
|
||||
get { return _viewer != null && _viewer.IsDocumentOpen; }
|
||||
}
|
||||
|
||||
/// <summary>扒图连续模式当前是否激活(供 ESC 全局过滤判断)。</summary>
|
||||
public static bool PickModeActive
|
||||
{
|
||||
get { return _viewer != null && _viewer.PickModeActive; }
|
||||
}
|
||||
|
||||
/// <summary>兼容 CadBridge:查看器中蒙版被外部删除(当前无触发源,保留事件形状)。</summary>
|
||||
public static event Action<string> EntityErased;
|
||||
|
||||
/// <summary>框选识别完成一批(图号/箱号/元件)。</summary>
|
||||
public static event Action<MyApp.PaMode, List<MyApp.PaTextResult>> PaPicked;
|
||||
|
||||
/// <summary>用户按 ESC 退出连续扒图模式。</summary>
|
||||
public static event Action PaExited;
|
||||
|
||||
/// <summary>用户按 F1 —— 元件两两合并。</summary>
|
||||
public static event Action F1Requested;
|
||||
|
||||
/// <summary>用户按 F2 —— 元件三个一组合并。</summary>
|
||||
public static event Action F2Requested;
|
||||
|
||||
/// <summary>用户按 F3 —— 元件四个一组合并。</summary>
|
||||
public static event Action F3Requested;
|
||||
|
||||
/// <summary>把 PDF 查看器挂到桥上(查看器构造时调用一次)。</summary>
|
||||
public static void Attach(PdfViewerWindow viewer)
|
||||
{
|
||||
_viewer = viewer;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 查看器 → Form3 的上行事件触发(仅查看器调用)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
public static void RaisePaPicked(MyApp.PaMode mode, List<MyApp.PaTextResult> results)
|
||||
{
|
||||
if (PaPicked != null) PaPicked(mode, results);
|
||||
}
|
||||
|
||||
public static void RaisePaExited()
|
||||
{
|
||||
if (PaExited != null) PaExited();
|
||||
}
|
||||
|
||||
public static void RaiseEntityErased(string handle)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(handle) && EntityErased != null)
|
||||
EntityErased(handle);
|
||||
}
|
||||
|
||||
public static void RaiseF1() { if (F1Requested != null) F1Requested(); }
|
||||
public static void RaiseF2() { if (F2Requested != null) F2Requested(); }
|
||||
public static void RaiseF3() { if (F3Requested != null) F3Requested(); }
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Form3 → 查看器的下行命令
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 发送扒图命令。接受原 CAD 命令名(CADPATU/CADPAXH/CADPAYJ/CADPACL)
|
||||
/// 或网页命令名(patu/paxh/payj/pacl)。
|
||||
/// </summary>
|
||||
public static void SendPaCommand(string cmd)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cmd)) return;
|
||||
string c = cmd.Trim().ToUpperInvariant();
|
||||
int mode = 0;
|
||||
if (c == "CADPATU" || c == "PATU") mode = 0;
|
||||
else if (c == "CADPAXH" || c == "PAXH") mode = 1;
|
||||
else if (c == "CADPAYJ" || c == "PAYJ") mode = 2;
|
||||
else if (c == "CADPACL" || c == "PACL")
|
||||
{
|
||||
ClearHighlight();
|
||||
return;
|
||||
}
|
||||
else return;
|
||||
|
||||
if (_viewer == null) return;
|
||||
_viewer.StartPick(mode);
|
||||
}
|
||||
|
||||
/// <summary>定位到指定 Handle 的蒙版框(点击表格行定位)。</summary>
|
||||
public static void ZoomToFrameHandle(string handle)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(handle) || _viewer == null) return;
|
||||
_viewer.ZoomToFrame(handle);
|
||||
}
|
||||
|
||||
/// <summary>批量删除指定 Handle 的蒙版(删表格行联动删框)。</summary>
|
||||
public static void DeleteFramesByHandles(IEnumerable<string> handles)
|
||||
{
|
||||
if (handles == null || _viewer == null) return;
|
||||
var list = new List<string>();
|
||||
foreach (string h in handles)
|
||||
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
|
||||
if (list.Count == 0) return;
|
||||
_viewer.DeleteFrames(list);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并蒙版:保留 keep 的蒙版并扩大到所有 removes 蒙版的并集,
|
||||
/// 被合并行的蒙版移除(元件合并时蒙版同步合并)。
|
||||
/// </summary>
|
||||
public static void MergeMasks(string keep, IEnumerable<string> removes)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keep) || _viewer == null) return;
|
||||
var list = new List<string>();
|
||||
if (removes != null)
|
||||
foreach (string h in removes)
|
||||
if (!string.IsNullOrWhiteSpace(h) && h != keep) list.Add(h);
|
||||
if (list.Count == 0) return;
|
||||
_viewer.MergeMasks(keep, list);
|
||||
}
|
||||
|
||||
/// <summary>清除所有高亮。</summary>
|
||||
public static void ClearHighlight()
|
||||
{
|
||||
if (_viewer == null) return;
|
||||
_viewer.ClearHighlight();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// ============================================================
|
||||
// PdfDocument:PDF 文档封装(加载/页尺寸/按缩放渲染页位图)
|
||||
// 坐标系:页面坐标 = PDF 点(1/72 英寸),原点左上(pdfium 渲染后
|
||||
// 已翻转为屏幕方向,y 向下)。
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>已打开的 PDF 文档。非线程安全,只在 UI 线程使用。</summary>
|
||||
internal sealed class PdfDocument : IDisposable
|
||||
{
|
||||
private IntPtr _doc = IntPtr.Zero;
|
||||
private IntPtr[] _pageHandles; // 惰性加载的页句柄缓存
|
||||
// ★ FPDF_LoadMemDocument 要求后备缓冲在文档关闭前保持有效且不可移动——
|
||||
// 原来读入的 byte[] 是局部变量,打开后就没人引用;打开下一个 PDF 的大分配
|
||||
// 触发 GC 把它搬走/回收,文档内存变垃圾 → 第二个 PDF 打不开(实测用户报障)。
|
||||
// 钉住(GCHandleType.Pinned)+字段保引用,Close 时才释放。
|
||||
private byte[] _bytes;
|
||||
private System.Runtime.InteropServices.GCHandle _pin;
|
||||
|
||||
public string FilePath { get; private set; }
|
||||
public int PageCount { get; private set; }
|
||||
public float[] PageWidthPt { get; private set; }
|
||||
public float[] PageHeightPt { get; private set; }
|
||||
|
||||
public bool IsOpen
|
||||
{
|
||||
get { return _doc != IntPtr.Zero; }
|
||||
}
|
||||
|
||||
/// <summary>打开 PDF(读入内存后走 FPDF_LoadMemDocument,免疫路径问题)。失败抛异常。</summary>
|
||||
public void Open(string path, string password)
|
||||
{
|
||||
Close();
|
||||
if (!File.Exists(path))
|
||||
throw new Exception("文件不存在: " + path);
|
||||
_bytes = File.ReadAllBytes(path);
|
||||
_pin = System.Runtime.InteropServices.GCHandle.Alloc(_bytes,
|
||||
System.Runtime.InteropServices.GCHandleType.Pinned);
|
||||
IntPtr doc = PdfiumNative.LoadDocumentFromBytes(_bytes, password);
|
||||
if (doc == IntPtr.Zero)
|
||||
{
|
||||
FreeBuffer();
|
||||
throw new Exception("PDF 打开失败:" + PdfiumNative.LastErrorText());
|
||||
}
|
||||
|
||||
int count = PdfiumNative.GetPageCount(doc);
|
||||
if (count <= 0)
|
||||
{
|
||||
PdfiumNative.CloseDocument(doc);
|
||||
FreeBuffer();
|
||||
throw new Exception("PDF 没有任何页面。");
|
||||
}
|
||||
|
||||
_doc = doc;
|
||||
FilePath = path;
|
||||
PageCount = count;
|
||||
_pageHandles = new IntPtr[count];
|
||||
PageWidthPt = new float[count];
|
||||
PageHeightPt = new float[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float w, h;
|
||||
if (!PdfiumNative.GetPageSize(doc, i, out w, out h))
|
||||
{
|
||||
w = 595f; h = 842f; // A4 兜底
|
||||
}
|
||||
PageWidthPt[i] = w;
|
||||
PageHeightPt[i] = h;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>释放文档后备缓冲(钉住句柄+字节数组)。</summary>
|
||||
private void FreeBuffer()
|
||||
{
|
||||
if (_pin.IsAllocated) _pin.Free();
|
||||
_bytes = null;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (_pageHandles != null)
|
||||
{
|
||||
for (int i = 0; i < _pageHandles.Length; i++)
|
||||
PdfiumNative.ClosePage(_pageHandles[i]);
|
||||
_pageHandles = null;
|
||||
}
|
||||
if (_doc != IntPtr.Zero)
|
||||
{
|
||||
PdfiumNative.CloseDocument(_doc);
|
||||
_doc = IntPtr.Zero;
|
||||
}
|
||||
FreeBuffer();
|
||||
FilePath = null;
|
||||
PageCount = 0;
|
||||
PageWidthPt = null;
|
||||
PageHeightPt = null;
|
||||
}
|
||||
|
||||
/// <summary>渲染指定页。scale = 像素/PDF点。失败返回 null。</summary>
|
||||
public Bitmap RenderPage(int pageIndex, float scale)
|
||||
{
|
||||
if (!IsOpen) return null;
|
||||
if (pageIndex < 0 || pageIndex >= PageCount) return null;
|
||||
IntPtr page = GetPage(pageIndex);
|
||||
if (page == IntPtr.Zero) return null;
|
||||
|
||||
int w = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * scale));
|
||||
int h = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * scale));
|
||||
return PdfiumNative.RenderPageToBitmap(page, w, h);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 高清渲染页面局部区域(OCR 用):与屏幕显示缩放无关,
|
||||
/// 用户任意缩放下框选,OCR 都拿到按 ocrScale 渲染的高清裁剪。
|
||||
/// 返回位图与 rectPt 一一对应(像素/pt = ocrScale)。
|
||||
/// </summary>
|
||||
public Bitmap RenderRegion(int pageIndex, RectangleF rectPt, float ocrScale)
|
||||
{
|
||||
if (!IsOpen) return null;
|
||||
if (pageIndex < 0 || pageIndex >= PageCount) return null;
|
||||
IntPtr page = GetPage(pageIndex);
|
||||
if (page == IntPtr.Zero) return null;
|
||||
|
||||
// 整页渲染尺寸(控制缩放)
|
||||
int fullW = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * ocrScale));
|
||||
int fullH = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * ocrScale));
|
||||
// 区域矩形(裁到页面范围)
|
||||
float pw = PageWidthPt[pageIndex], ph = PageHeightPt[pageIndex];
|
||||
float l = Math.Max(0, rectPt.X), t = Math.Max(0, rectPt.Y);
|
||||
float r = Math.Min(pw, rectPt.Right), b = Math.Min(ph, rectPt.Bottom);
|
||||
if (r - l < 1 || b - t < 1) return null;
|
||||
int startX = (int)Math.Floor(l * ocrScale);
|
||||
int startY = (int)Math.Floor(t * ocrScale);
|
||||
int endX = (int)Math.Ceiling(r * ocrScale);
|
||||
int endY = (int)Math.Ceiling(b * ocrScale);
|
||||
return PdfiumNative.RenderRegionToBitmap(page, startX, startY,
|
||||
endX - startX, endY - startY, fullW, fullH);
|
||||
}
|
||||
|
||||
private IntPtr GetPage(int pageIndex)
|
||||
{
|
||||
IntPtr page = _pageHandles[pageIndex];
|
||||
if (page == IntPtr.Zero)
|
||||
{
|
||||
page = PdfiumNative.LoadPage(_doc, pageIndex);
|
||||
_pageHandles[pageIndex] = page;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,228 @@
|
||||
// ============================================================
|
||||
// PdfiumNative:pdfium.dll 原生库 P/Invoke 封装
|
||||
// dll 来源:pypdfium2 win32 wheel(pypdfium2_raw/pdfium.dll,x86)
|
||||
// 调用约定:cdecl(pypdfium2 ctypes.CDLL 已验证)
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>pdfium C API 最小封装(仅本项目用到的函数)。</summary>
|
||||
internal static class PdfiumNative
|
||||
{
|
||||
public const string DllName = "pdfium.dll";
|
||||
|
||||
// 渲染标志:渲染注释/批注
|
||||
public const int FPDF_ANNOT = 0x01;
|
||||
|
||||
// FPDF_ERR_* 错误码(GetLastError 返回)
|
||||
public const int FPDF_ERR_SUCCESS = 0; // 无错误
|
||||
public const int FPDF_ERR_UNKNOWN = 1; // 未知错误
|
||||
public const int FPDF_ERR_FILE = 2; // 文件无法打开或读取
|
||||
public const int FPDF_ERR_FORMAT = 3; // 文件不是 PDF 或已损坏
|
||||
public const int FPDF_ERR_PASSWORD = 4; // 需要密码或密码错误
|
||||
public const int FPDF_ERR_SECURITY = 5; // 不支持的安全方案
|
||||
public const int FPDF_ERR_PAGE = 6; // 页面无法加载
|
||||
|
||||
private static bool _inited;
|
||||
private static readonly object _initLock = new object();
|
||||
|
||||
/// <summary>初始化 pdfium(进程内只需一次,线程安全)。</summary>
|
||||
public static void InitLibrary()
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
if (_inited) return;
|
||||
FPDF_InitLibrary();
|
||||
_inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDF_InitLibrary();
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDF_DestroyLibrary();
|
||||
|
||||
[DllImport(DllName, CharSet = CharSet.Ansi)]
|
||||
private static extern IntPtr FPDF_LoadDocument(string file_path, string password);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDF_CloseDocument(IntPtr document);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern uint FPDF_GetLastError();
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern int FPDF_GetPageCount(IntPtr document);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern IntPtr FPDF_LoadPage(IntPtr document, int page_index);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDF_ClosePage(IntPtr page);
|
||||
|
||||
// ★ 新版 GetPageSizeByIndexF 是 FS_SIZEF* 参数(易错);用稳定的旧版 double API
|
||||
[DllImport(DllName)]
|
||||
private static extern int FPDF_GetPageSizeByIndex(IntPtr document, int page_index,
|
||||
out double width, out double height);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern IntPtr FPDFBitmap_Create(int width, int height, int alpha);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDFBitmap_FillRect(IntPtr bitmap, int left, int top,
|
||||
int width, int height, uint color);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDFBitmap_Destroy(IntPtr bitmap);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern IntPtr FPDFBitmap_GetBuffer(IntPtr bitmap);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern int FPDFBitmap_GetStride(IntPtr bitmap);
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern void FPDF_RenderPageBitmap(IntPtr bitmap, IntPtr page,
|
||||
int start_x, int start_y, int size_x, int size_y,
|
||||
int rotate, int flags);
|
||||
|
||||
|
||||
[DllImport(DllName)]
|
||||
private static extern IntPtr FPDF_LoadMemDocument(byte[] data, int size, string password);
|
||||
|
||||
public static IntPtr LoadDocumentFromBytes(byte[] data, string password)
|
||||
{
|
||||
InitLibrary();
|
||||
if (data == null || data.Length == 0) return IntPtr.Zero;
|
||||
return FPDF_LoadMemDocument(data, data.Length, string.IsNullOrEmpty(password) ? null : password);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 上层直接使用的安全包装
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// <summary>打开 PDF 文件。失败返回 IntPtr.Zero,可用 LastErrorText 查原因。</summary>
|
||||
public static IntPtr LoadDocument(string path, string password)
|
||||
{
|
||||
InitLibrary();
|
||||
if (string.IsNullOrEmpty(path)) return IntPtr.Zero;
|
||||
return FPDF_LoadDocument(path, string.IsNullOrEmpty(password) ? null : password);
|
||||
}
|
||||
|
||||
public static void CloseDocument(IntPtr doc)
|
||||
{
|
||||
if (doc != IntPtr.Zero) FPDF_CloseDocument(doc);
|
||||
}
|
||||
|
||||
public static int GetPageCount(IntPtr doc)
|
||||
{
|
||||
if (doc == IntPtr.Zero) return 0;
|
||||
return FPDF_GetPageCount(doc);
|
||||
}
|
||||
|
||||
/// <summary>取指定页尺寸(PDF 点,1/72 英寸)。失败返回 false。</summary>
|
||||
public static bool GetPageSize(IntPtr doc, int pageIndex, out float widthPt, out float heightPt)
|
||||
{
|
||||
widthPt = 0; heightPt = 0;
|
||||
if (doc == IntPtr.Zero) return false;
|
||||
double w, h;
|
||||
bool ok = FPDF_GetPageSizeByIndex(doc, pageIndex, out w, out h) != 0;
|
||||
widthPt = (float)w;
|
||||
heightPt = (float)h;
|
||||
return ok;
|
||||
}
|
||||
|
||||
public static IntPtr LoadPage(IntPtr doc, int pageIndex)
|
||||
{
|
||||
if (doc == IntPtr.Zero) return IntPtr.Zero;
|
||||
return FPDF_LoadPage(doc, pageIndex);
|
||||
}
|
||||
|
||||
public static void ClosePage(IntPtr page)
|
||||
{
|
||||
if (page != IntPtr.Zero) FPDF_ClosePage(page);
|
||||
}
|
||||
|
||||
/// <summary>把整页渲染到 GDI+ 位图(白底不透明)。</summary>
|
||||
public static System.Drawing.Bitmap RenderPageToBitmap(IntPtr page, int widthPx, int heightPx)
|
||||
{
|
||||
return RenderRegionToBitmap(page, 0, 0, widthPx, heightPx, widthPx, heightPx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把页面局部区域渲染到位图(白底不透明)。
|
||||
/// 页面整图尺寸 = fullW×fullH 像素;只取 (startX,startY,wPx,hPx) 矩形。
|
||||
/// 用于:OCR 框选区域按任意分辨率高清重渲染(与显示缩放无关)。
|
||||
/// </summary>
|
||||
public static System.Drawing.Bitmap RenderRegionToBitmap(IntPtr page,
|
||||
int startX, int startY, int wPx, int hPx, int fullW, int fullH)
|
||||
{
|
||||
if (page == IntPtr.Zero || wPx <= 0 || hPx <= 0) return null;
|
||||
if (wPx > 16384 || hPx > 16384) return null;
|
||||
|
||||
IntPtr bmp = FPDFBitmap_Create(wPx, hPx, 1);
|
||||
if (bmp == IntPtr.Zero) return null;
|
||||
try
|
||||
{
|
||||
// 白底不透明(0xAARRGGBB)
|
||||
FPDFBitmap_FillRect(bmp, 0, 0, wPx, hPx, 0xFFFFFFFF);
|
||||
// start_x/start_y 为负偏移:位图原点相对页面原点
|
||||
FPDF_RenderPageBitmap(bmp, page, -startX, -startY, fullW, fullH, 0, FPDF_ANNOT);
|
||||
|
||||
IntPtr buffer = FPDFBitmap_GetBuffer(bmp);
|
||||
int stride = FPDFBitmap_GetStride(bmp);
|
||||
if (buffer == IntPtr.Zero || stride <= 0) return null;
|
||||
|
||||
// pdfium BGRA(top-down) → GDI+ Format32bppArgb(BGRA) 逐行拷贝
|
||||
var result = new System.Drawing.Bitmap(wPx, hPx,
|
||||
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||||
var rect = new System.Drawing.Rectangle(0, 0, wPx, hPx);
|
||||
var data = result.LockBits(rect,
|
||||
System.Drawing.Imaging.ImageLockMode.WriteOnly,
|
||||
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
|
||||
try
|
||||
{
|
||||
for (int y = 0; y < hPx; y++)
|
||||
{
|
||||
IntPtr src = new IntPtr(buffer.ToInt64() + (long)y * stride);
|
||||
IntPtr dst = new IntPtr(data.Scan0.ToInt64() + (long)y * data.Stride);
|
||||
NativeMethods.CopyMemory(dst, src, (uint)(wPx * 4));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
result.UnlockBits(data);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
{
|
||||
FPDFBitmap_Destroy(bmp);
|
||||
}
|
||||
}
|
||||
|
||||
public static string LastErrorText()
|
||||
{
|
||||
uint err = FPDF_GetLastError();
|
||||
switch (err)
|
||||
{
|
||||
case FPDF_ERR_SUCCESS: return "成功";
|
||||
case FPDF_ERR_FILE: return "文件无法打开或读取";
|
||||
case FPDF_ERR_FORMAT: return "文件不是有效 PDF 或已损坏";
|
||||
case FPDF_ERR_PASSWORD: return "PDF 已加密,需要密码";
|
||||
case FPDF_ERR_SECURITY: return "不支持的安全方案";
|
||||
case FPDF_ERR_PAGE: return "页面内容无法解析";
|
||||
default: return "未知错误(" + err + ")";
|
||||
}
|
||||
}
|
||||
|
||||
private static class NativeMethods
|
||||
{
|
||||
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
|
||||
public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1321
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,29 @@
|
||||
// ============================================================
|
||||
// 扒图数据类型(与原 cadcj 插件 / 网页端 cadbridge.ts 保持一致)
|
||||
// ============================================================
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MyApp
|
||||
{
|
||||
/// <summary>
|
||||
/// 扒数据模式:图号/箱号/元件
|
||||
/// </summary>
|
||||
public enum PaMode
|
||||
{
|
||||
Tuhao, // 扒图号
|
||||
Xianghao, // 扒箱号
|
||||
Yuanjian // 扒元件
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扒图结果:一条文本 + 它对应的彩色框的 Handle(句柄字符串)
|
||||
/// 用于关联表格数据行与网页 CAD 框实体,实现点击行定位、删除行删框。
|
||||
/// ★ TextHandle:源文本实体自身的 Handle,用于判断"是否同一个文本"。
|
||||
/// </summary>
|
||||
public class PaTextResult
|
||||
{
|
||||
public string Text;
|
||||
public string FrameHandle;
|
||||
public string TextHandle = "";
|
||||
}
|
||||
}
|
||||
Generated
+609
@@ -0,0 +1,609 @@
|
||||
namespace MyApp
|
||||
{
|
||||
public partial class 新建项目 : System.Windows.Forms.Form
|
||||
{
|
||||
/// <summary>必需的设计器变量。</summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>清理正在使用的资源。</summary>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows 窗体设计器生成的代码
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
this.lbl项目编号 = new System.Windows.Forms.Label();
|
||||
this.txt项目编号 = new System.Windows.Forms.TextBox();
|
||||
this.lbl项目日期 = new System.Windows.Forms.Label();
|
||||
this.txt项目日期 = new System.Windows.Forms.TextBox();
|
||||
this.lbl项目名称 = new System.Windows.Forms.Label();
|
||||
this.txt项目名称 = new System.Windows.Forms.TextBox();
|
||||
this.lbl客户名称 = new System.Windows.Forms.Label();
|
||||
this.txt客户名称 = new System.Windows.Forms.TextBox();
|
||||
this.btn选择客户 = new System.Windows.Forms.Button();
|
||||
this.lbl客户地址 = new System.Windows.Forms.Label();
|
||||
this.txt客户地址 = new System.Windows.Forms.TextBox();
|
||||
this.lbl联系人 = new System.Windows.Forms.Label();
|
||||
this.txt联系人 = new System.Windows.Forms.TextBox();
|
||||
this.lbl联系电话 = new System.Windows.Forms.Label();
|
||||
this.txt联系电话 = new System.Windows.Forms.TextBox();
|
||||
this.lbl成套厂名称 = new System.Windows.Forms.Label();
|
||||
this.txt成套厂名称 = new System.Windows.Forms.TextBox();
|
||||
this.lbl成套厂地址 = new System.Windows.Forms.Label();
|
||||
this.txt成套厂地址 = new System.Windows.Forms.TextBox();
|
||||
this.lbl成套厂联系人 = new System.Windows.Forms.Label();
|
||||
this.txt成套厂联系人 = new System.Windows.Forms.TextBox();
|
||||
this.lbl成套厂联系电话 = new System.Windows.Forms.Label();
|
||||
this.txt成套厂联系电话 = new System.Windows.Forms.TextBox();
|
||||
this.lbl报出方式 = new System.Windows.Forms.Label();
|
||||
this.cmb报出方式 = new System.Windows.Forms.ComboBox();
|
||||
this.lbl箱柜模板 = new System.Windows.Forms.Label();
|
||||
this.cmb箱柜模板 = new System.Windows.Forms.ComboBox();
|
||||
this.lbl单台价格 = new System.Windows.Forms.Label();
|
||||
this.cmb单台价格 = new System.Windows.Forms.ComboBox();
|
||||
this.lbl项目总价 = new System.Windows.Forms.Label();
|
||||
this.lbl备注 = new System.Windows.Forms.Label();
|
||||
this.txt备注 = new System.Windows.Forms.TextBox();
|
||||
this.btn确定 = new System.Windows.Forms.Button();
|
||||
this.btn取消 = new System.Windows.Forms.Button();
|
||||
this.comboBox1 = new System.Windows.Forms.ComboBox();
|
||||
this.label1 = new System.Windows.Forms.Label();
|
||||
this.label2 = new System.Windows.Forms.Label();
|
||||
this.comboBox2 = new System.Windows.Forms.ComboBox();
|
||||
this.cmb项目总价 = new System.Windows.Forms.ComboBox();
|
||||
this.SuspendLayout();
|
||||
//
|
||||
// lbl项目编号
|
||||
//
|
||||
this.lbl项目编号.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl项目编号.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl项目编号.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl项目编号.Location = new System.Drawing.Point(20, 25);
|
||||
this.lbl项目编号.Name = "lbl项目编号";
|
||||
this.lbl项目编号.Size = new System.Drawing.Size(70, 23);
|
||||
this.lbl项目编号.TabIndex = 0;
|
||||
this.lbl项目编号.Text = "项目编号:";
|
||||
this.lbl项目编号.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt项目编号
|
||||
//
|
||||
this.txt项目编号.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt项目编号.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt项目编号.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt项目编号.Location = new System.Drawing.Point(95, 25);
|
||||
this.txt项目编号.Name = "txt项目编号";
|
||||
this.txt项目编号.Size = new System.Drawing.Size(220, 28);
|
||||
this.txt项目编号.TabIndex = 0;
|
||||
//
|
||||
// lbl项目日期
|
||||
//
|
||||
this.lbl项目日期.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl项目日期.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl项目日期.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl项目日期.Location = new System.Drawing.Point(380, 28);
|
||||
this.lbl项目日期.Name = "lbl项目日期";
|
||||
this.lbl项目日期.Size = new System.Drawing.Size(70, 18);
|
||||
this.lbl项目日期.TabIndex = 1;
|
||||
this.lbl项目日期.Text = "项目日期:";
|
||||
//
|
||||
// txt项目日期
|
||||
//
|
||||
this.txt项目日期.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt项目日期.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt项目日期.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt项目日期.Location = new System.Drawing.Point(455, 25);
|
||||
this.txt项目日期.Name = "txt项目日期";
|
||||
this.txt项目日期.Size = new System.Drawing.Size(210, 28);
|
||||
this.txt项目日期.TabIndex = 1;
|
||||
//
|
||||
// lbl项目名称
|
||||
//
|
||||
this.lbl项目名称.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl项目名称.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl项目名称.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl项目名称.Location = new System.Drawing.Point(20, 55);
|
||||
this.lbl项目名称.Name = "lbl项目名称";
|
||||
this.lbl项目名称.Size = new System.Drawing.Size(70, 23);
|
||||
this.lbl项目名称.TabIndex = 2;
|
||||
this.lbl项目名称.Text = "项目名称:";
|
||||
this.lbl项目名称.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt项目名称
|
||||
//
|
||||
this.txt项目名称.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt项目名称.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt项目名称.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt项目名称.Location = new System.Drawing.Point(95, 55);
|
||||
this.txt项目名称.Name = "txt项目名称";
|
||||
this.txt项目名称.Size = new System.Drawing.Size(570, 28);
|
||||
this.txt项目名称.TabIndex = 2;
|
||||
//
|
||||
// lbl客户名称
|
||||
//
|
||||
this.lbl客户名称.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl客户名称.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl客户名称.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl客户名称.Location = new System.Drawing.Point(20, 85);
|
||||
this.lbl客户名称.Name = "lbl客户名称";
|
||||
this.lbl客户名称.Size = new System.Drawing.Size(70, 23);
|
||||
this.lbl客户名称.TabIndex = 3;
|
||||
this.lbl客户名称.Text = "客户名称:";
|
||||
this.lbl客户名称.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt客户名称
|
||||
//
|
||||
this.txt客户名称.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt客户名称.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt客户名称.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt客户名称.Location = new System.Drawing.Point(95, 85);
|
||||
this.txt客户名称.Name = "txt客户名称";
|
||||
this.txt客户名称.Size = new System.Drawing.Size(479, 28);
|
||||
this.txt客户名称.TabIndex = 3;
|
||||
//
|
||||
// btn选择客户
|
||||
//
|
||||
this.btn选择客户.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.btn选择客户.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.btn选择客户.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.btn选择客户.Location = new System.Drawing.Point(580, 85);
|
||||
this.btn选择客户.Name = "btn选择客户";
|
||||
this.btn选择客户.Size = new System.Drawing.Size(85, 28);
|
||||
this.btn选择客户.TabIndex = 4;
|
||||
this.btn选择客户.Text = "选择客户";
|
||||
this.btn选择客户.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// lbl客户地址
|
||||
//
|
||||
this.lbl客户地址.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl客户地址.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl客户地址.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl客户地址.Location = new System.Drawing.Point(20, 115);
|
||||
this.lbl客户地址.Name = "lbl客户地址";
|
||||
this.lbl客户地址.Size = new System.Drawing.Size(70, 23);
|
||||
this.lbl客户地址.TabIndex = 5;
|
||||
this.lbl客户地址.Text = "客户地址:";
|
||||
this.lbl客户地址.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt客户地址
|
||||
//
|
||||
this.txt客户地址.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt客户地址.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt客户地址.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt客户地址.Location = new System.Drawing.Point(95, 115);
|
||||
this.txt客户地址.Name = "txt客户地址";
|
||||
this.txt客户地址.Size = new System.Drawing.Size(570, 28);
|
||||
this.txt客户地址.TabIndex = 5;
|
||||
//
|
||||
// lbl联系人
|
||||
//
|
||||
this.lbl联系人.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl联系人.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl联系人.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl联系人.Location = new System.Drawing.Point(30, 145);
|
||||
this.lbl联系人.Name = "lbl联系人";
|
||||
this.lbl联系人.Size = new System.Drawing.Size(60, 23);
|
||||
this.lbl联系人.TabIndex = 6;
|
||||
this.lbl联系人.Text = "联系人:";
|
||||
this.lbl联系人.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt联系人
|
||||
//
|
||||
this.txt联系人.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt联系人.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt联系人.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt联系人.Location = new System.Drawing.Point(95, 145);
|
||||
this.txt联系人.Name = "txt联系人";
|
||||
this.txt联系人.Size = new System.Drawing.Size(220, 28);
|
||||
this.txt联系人.TabIndex = 6;
|
||||
//
|
||||
// lbl联系电话
|
||||
//
|
||||
this.lbl联系电话.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl联系电话.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl联系电话.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl联系电话.Location = new System.Drawing.Point(380, 145);
|
||||
this.lbl联系电话.Name = "lbl联系电话";
|
||||
this.lbl联系电话.Size = new System.Drawing.Size(70, 21);
|
||||
this.lbl联系电话.TabIndex = 7;
|
||||
this.lbl联系电话.Text = "联系电话:";
|
||||
//
|
||||
// txt联系电话
|
||||
//
|
||||
this.txt联系电话.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt联系电话.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt联系电话.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt联系电话.Location = new System.Drawing.Point(455, 145);
|
||||
this.txt联系电话.Name = "txt联系电话";
|
||||
this.txt联系电话.Size = new System.Drawing.Size(210, 28);
|
||||
this.txt联系电话.TabIndex = 7;
|
||||
//
|
||||
// lbl成套厂名称
|
||||
//
|
||||
this.lbl成套厂名称.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl成套厂名称.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl成套厂名称.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl成套厂名称.Location = new System.Drawing.Point(10, 175);
|
||||
this.lbl成套厂名称.Name = "lbl成套厂名称";
|
||||
this.lbl成套厂名称.Size = new System.Drawing.Size(80, 23);
|
||||
this.lbl成套厂名称.TabIndex = 8;
|
||||
this.lbl成套厂名称.Text = "成套厂名称:";
|
||||
this.lbl成套厂名称.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt成套厂名称
|
||||
//
|
||||
this.txt成套厂名称.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt成套厂名称.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt成套厂名称.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt成套厂名称.Location = new System.Drawing.Point(95, 175);
|
||||
this.txt成套厂名称.Name = "txt成套厂名称";
|
||||
this.txt成套厂名称.Size = new System.Drawing.Size(570, 28);
|
||||
this.txt成套厂名称.TabIndex = 8;
|
||||
//
|
||||
// lbl成套厂地址
|
||||
//
|
||||
this.lbl成套厂地址.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl成套厂地址.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl成套厂地址.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl成套厂地址.Location = new System.Drawing.Point(10, 205);
|
||||
this.lbl成套厂地址.Name = "lbl成套厂地址";
|
||||
this.lbl成套厂地址.Size = new System.Drawing.Size(80, 23);
|
||||
this.lbl成套厂地址.TabIndex = 9;
|
||||
this.lbl成套厂地址.Text = "成套厂地址:";
|
||||
this.lbl成套厂地址.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt成套厂地址
|
||||
//
|
||||
this.txt成套厂地址.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt成套厂地址.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt成套厂地址.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt成套厂地址.Location = new System.Drawing.Point(95, 205);
|
||||
this.txt成套厂地址.Name = "txt成套厂地址";
|
||||
this.txt成套厂地址.Size = new System.Drawing.Size(570, 28);
|
||||
this.txt成套厂地址.TabIndex = 9;
|
||||
//
|
||||
// lbl成套厂联系人
|
||||
//
|
||||
this.lbl成套厂联系人.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl成套厂联系人.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl成套厂联系人.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl成套厂联系人.Location = new System.Drawing.Point(30, 235);
|
||||
this.lbl成套厂联系人.Name = "lbl成套厂联系人";
|
||||
this.lbl成套厂联系人.Size = new System.Drawing.Size(60, 23);
|
||||
this.lbl成套厂联系人.TabIndex = 10;
|
||||
this.lbl成套厂联系人.Text = "联系人:";
|
||||
this.lbl成套厂联系人.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt成套厂联系人
|
||||
//
|
||||
this.txt成套厂联系人.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt成套厂联系人.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt成套厂联系人.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt成套厂联系人.Location = new System.Drawing.Point(95, 235);
|
||||
this.txt成套厂联系人.Name = "txt成套厂联系人";
|
||||
this.txt成套厂联系人.Size = new System.Drawing.Size(220, 28);
|
||||
this.txt成套厂联系人.TabIndex = 10;
|
||||
//
|
||||
// lbl成套厂联系电话
|
||||
//
|
||||
this.lbl成套厂联系电话.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl成套厂联系电话.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl成套厂联系电话.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl成套厂联系电话.Location = new System.Drawing.Point(380, 235);
|
||||
this.lbl成套厂联系电话.Name = "lbl成套厂联系电话";
|
||||
this.lbl成套厂联系电话.Size = new System.Drawing.Size(70, 21);
|
||||
this.lbl成套厂联系电话.TabIndex = 11;
|
||||
this.lbl成套厂联系电话.Text = "联系电话:";
|
||||
//
|
||||
// txt成套厂联系电话
|
||||
//
|
||||
this.txt成套厂联系电话.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt成套厂联系电话.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt成套厂联系电话.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt成套厂联系电话.Location = new System.Drawing.Point(455, 232);
|
||||
this.txt成套厂联系电话.Name = "txt成套厂联系电话";
|
||||
this.txt成套厂联系电话.Size = new System.Drawing.Size(210, 28);
|
||||
this.txt成套厂联系电话.TabIndex = 11;
|
||||
//
|
||||
// lbl报出方式
|
||||
//
|
||||
this.lbl报出方式.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl报出方式.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl报出方式.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl报出方式.Location = new System.Drawing.Point(20, 265);
|
||||
this.lbl报出方式.Name = "lbl报出方式";
|
||||
this.lbl报出方式.Size = new System.Drawing.Size(70, 23);
|
||||
this.lbl报出方式.TabIndex = 12;
|
||||
this.lbl报出方式.Text = "报出方式:";
|
||||
this.lbl报出方式.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// cmb报出方式
|
||||
//
|
||||
this.cmb报出方式.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.cmb报出方式.DropDownWidth = 200;
|
||||
this.cmb报出方式.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.cmb报出方式.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.cmb报出方式.ItemHeight = 18;
|
||||
this.cmb报出方式.Location = new System.Drawing.Point(95, 265);
|
||||
this.cmb报出方式.Name = "cmb报出方式";
|
||||
this.cmb报出方式.Size = new System.Drawing.Size(220, 26);
|
||||
this.cmb报出方式.TabIndex = 12;
|
||||
this.cmb报出方式.Text = "报出价=表价*采购系数*报出系数";
|
||||
//
|
||||
// lbl箱柜模板
|
||||
//
|
||||
this.lbl箱柜模板.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl箱柜模板.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl箱柜模板.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl箱柜模板.Location = new System.Drawing.Point(380, 265);
|
||||
this.lbl箱柜模板.Name = "lbl箱柜模板";
|
||||
this.lbl箱柜模板.Size = new System.Drawing.Size(70, 20);
|
||||
this.lbl箱柜模板.TabIndex = 13;
|
||||
this.lbl箱柜模板.Text = "箱柜模板:";
|
||||
//
|
||||
// cmb箱柜模板
|
||||
//
|
||||
this.cmb箱柜模板.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.cmb箱柜模板.DropDownWidth = 200;
|
||||
this.cmb箱柜模板.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.cmb箱柜模板.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.cmb箱柜模板.ItemHeight = 18;
|
||||
this.cmb箱柜模板.Location = new System.Drawing.Point(455, 262);
|
||||
this.cmb箱柜模板.Name = "cmb箱柜模板";
|
||||
this.cmb箱柜模板.Size = new System.Drawing.Size(210, 26);
|
||||
this.cmb箱柜模板.TabIndex = 13;
|
||||
//
|
||||
// lbl单台价格
|
||||
//
|
||||
this.lbl单台价格.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl单台价格.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl单台价格.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl单台价格.Location = new System.Drawing.Point(20, 293);
|
||||
this.lbl单台价格.Name = "lbl单台价格";
|
||||
this.lbl单台价格.Size = new System.Drawing.Size(70, 23);
|
||||
this.lbl单台价格.TabIndex = 14;
|
||||
this.lbl单台价格.Text = "单台价格:";
|
||||
this.lbl单台价格.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// cmb单台价格
|
||||
//
|
||||
this.cmb单台价格.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.cmb单台价格.DropDownWidth = 200;
|
||||
this.cmb单台价格.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.cmb单台价格.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.cmb单台价格.ItemHeight = 18;
|
||||
this.cmb单台价格.Location = new System.Drawing.Point(95, 293);
|
||||
this.cmb单台价格.Name = "cmb单台价格";
|
||||
this.cmb单台价格.Size = new System.Drawing.Size(220, 26);
|
||||
this.cmb单台价格.TabIndex = 14;
|
||||
this.cmb单台价格.Text = "保留整数";
|
||||
//
|
||||
// lbl项目总价
|
||||
//
|
||||
this.lbl项目总价.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl项目总价.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl项目总价.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl项目总价.Location = new System.Drawing.Point(379, 293);
|
||||
this.lbl项目总价.Name = "lbl项目总价";
|
||||
this.lbl项目总价.Size = new System.Drawing.Size(70, 20);
|
||||
this.lbl项目总价.TabIndex = 15;
|
||||
this.lbl项目总价.Text = "项目总价:";
|
||||
//
|
||||
// lbl备注
|
||||
//
|
||||
this.lbl备注.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.lbl备注.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.lbl备注.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.lbl备注.Location = new System.Drawing.Point(44, 352);
|
||||
this.lbl备注.Name = "lbl备注";
|
||||
this.lbl备注.Size = new System.Drawing.Size(46, 23);
|
||||
this.lbl备注.TabIndex = 16;
|
||||
this.lbl备注.Text = "备注:";
|
||||
this.lbl备注.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// txt备注
|
||||
//
|
||||
this.txt备注.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.txt备注.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.txt备注.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.txt备注.Location = new System.Drawing.Point(95, 352);
|
||||
this.txt备注.Multiline = true;
|
||||
this.txt备注.Name = "txt备注";
|
||||
this.txt备注.Size = new System.Drawing.Size(570, 80);
|
||||
this.txt备注.TabIndex = 16;
|
||||
//
|
||||
// btn确定
|
||||
//
|
||||
this.btn确定.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.btn确定.DialogResult = System.Windows.Forms.DialogResult.OK;
|
||||
this.btn确定.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.btn确定.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.btn确定.Location = new System.Drawing.Point(455, 449);
|
||||
this.btn确定.Name = "btn确定";
|
||||
this.btn确定.Size = new System.Drawing.Size(100, 28);
|
||||
this.btn确定.TabIndex = 17;
|
||||
this.btn确定.Text = "确定";
|
||||
this.btn确定.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// btn取消
|
||||
//
|
||||
this.btn取消.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.btn取消.DialogResult = System.Windows.Forms.DialogResult.Cancel;
|
||||
this.btn取消.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.btn取消.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.btn取消.Location = new System.Drawing.Point(565, 449);
|
||||
this.btn取消.Name = "btn取消";
|
||||
this.btn取消.Size = new System.Drawing.Size(100, 28);
|
||||
this.btn取消.TabIndex = 18;
|
||||
this.btn取消.Text = "取消";
|
||||
this.btn取消.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// comboBox1
|
||||
//
|
||||
this.comboBox1.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.comboBox1.DropDownWidth = 210;
|
||||
this.comboBox1.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.comboBox1.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.comboBox1.ItemHeight = 18;
|
||||
this.comboBox1.Location = new System.Drawing.Point(455, 322);
|
||||
this.comboBox1.Name = "comboBox1";
|
||||
this.comboBox1.Size = new System.Drawing.Size(210, 26);
|
||||
this.comboBox1.TabIndex = 14;
|
||||
this.comboBox1.Text = "四舍五入";
|
||||
//
|
||||
// label1
|
||||
//
|
||||
this.label1.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.label1.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.label1.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.label1.Location = new System.Drawing.Point(19, 320);
|
||||
this.label1.Name = "label1";
|
||||
this.label1.Size = new System.Drawing.Size(70, 23);
|
||||
this.label1.TabIndex = 14;
|
||||
this.label1.Text = "单台取整:";
|
||||
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||
//
|
||||
// label2
|
||||
//
|
||||
this.label2.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.label2.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.label2.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.label2.Location = new System.Drawing.Point(379, 324);
|
||||
this.label2.Name = "label2";
|
||||
this.label2.Size = new System.Drawing.Size(70, 20);
|
||||
this.label2.TabIndex = 15;
|
||||
this.label2.Text = "项目取整:";
|
||||
//
|
||||
// comboBox2
|
||||
//
|
||||
this.comboBox2.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.comboBox2.DropDownWidth = 220;
|
||||
this.comboBox2.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.comboBox2.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.comboBox2.ItemHeight = 18;
|
||||
this.comboBox2.Location = new System.Drawing.Point(95, 324);
|
||||
this.comboBox2.Name = "comboBox2";
|
||||
this.comboBox2.Size = new System.Drawing.Size(220, 26);
|
||||
this.comboBox2.TabIndex = 14;
|
||||
this.comboBox2.Text = "四舍五入";
|
||||
//
|
||||
// cmb项目总价
|
||||
//
|
||||
this.cmb项目总价.BackColor = System.Drawing.SystemColors.Window;
|
||||
this.cmb项目总价.DropDownWidth = 200;
|
||||
this.cmb项目总价.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.cmb项目总价.ForeColor = System.Drawing.SystemColors.WindowText;
|
||||
this.cmb项目总价.ItemHeight = 18;
|
||||
this.cmb项目总价.Location = new System.Drawing.Point(455, 290);
|
||||
this.cmb项目总价.Name = "cmb项目总价";
|
||||
this.cmb项目总价.Size = new System.Drawing.Size(210, 26);
|
||||
this.cmb项目总价.TabIndex = 14;
|
||||
this.cmb项目总价.Text = "保留整数";
|
||||
//
|
||||
// 新建项目
|
||||
//
|
||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||
this.AutoValidate = System.Windows.Forms.AutoValidate.EnablePreventFocusChange;
|
||||
this.BackColor = System.Drawing.SystemColors.Control;
|
||||
this.ClientSize = new System.Drawing.Size(678, 482);
|
||||
this.Controls.Add(this.lbl项目编号);
|
||||
this.Controls.Add(this.txt项目编号);
|
||||
this.Controls.Add(this.lbl项目日期);
|
||||
this.Controls.Add(this.txt项目日期);
|
||||
this.Controls.Add(this.lbl项目名称);
|
||||
this.Controls.Add(this.txt项目名称);
|
||||
this.Controls.Add(this.lbl客户名称);
|
||||
this.Controls.Add(this.txt客户名称);
|
||||
this.Controls.Add(this.btn选择客户);
|
||||
this.Controls.Add(this.lbl客户地址);
|
||||
this.Controls.Add(this.txt客户地址);
|
||||
this.Controls.Add(this.lbl联系人);
|
||||
this.Controls.Add(this.txt联系人);
|
||||
this.Controls.Add(this.lbl联系电话);
|
||||
this.Controls.Add(this.txt联系电话);
|
||||
this.Controls.Add(this.lbl成套厂名称);
|
||||
this.Controls.Add(this.txt成套厂名称);
|
||||
this.Controls.Add(this.lbl成套厂地址);
|
||||
this.Controls.Add(this.txt成套厂地址);
|
||||
this.Controls.Add(this.lbl成套厂联系人);
|
||||
this.Controls.Add(this.txt成套厂联系人);
|
||||
this.Controls.Add(this.lbl成套厂联系电话);
|
||||
this.Controls.Add(this.txt成套厂联系电话);
|
||||
this.Controls.Add(this.lbl报出方式);
|
||||
this.Controls.Add(this.cmb报出方式);
|
||||
this.Controls.Add(this.lbl箱柜模板);
|
||||
this.Controls.Add(this.cmb箱柜模板);
|
||||
this.Controls.Add(this.lbl单台价格);
|
||||
this.Controls.Add(this.cmb单台价格);
|
||||
this.Controls.Add(this.lbl项目总价);
|
||||
this.Controls.Add(this.lbl备注);
|
||||
this.Controls.Add(this.txt备注);
|
||||
this.Controls.Add(this.btn确定);
|
||||
this.Controls.Add(this.btn取消);
|
||||
this.Controls.Add(this.comboBox1);
|
||||
this.Controls.Add(this.label1);
|
||||
this.Controls.Add(this.label2);
|
||||
this.Controls.Add(this.comboBox2);
|
||||
this.Controls.Add(this.cmb项目总价);
|
||||
this.Font = new System.Drawing.Font("宋体", 9F);
|
||||
this.ForeColor = System.Drawing.SystemColors.ControlText;
|
||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
||||
this.MaximizeBox = false;
|
||||
this.MinimizeBox = false;
|
||||
this.Name = "新建项目";
|
||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
|
||||
this.Text = "新建项目";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private System.Windows.Forms.Label lbl项目编号;
|
||||
private System.Windows.Forms.TextBox txt项目编号;
|
||||
private System.Windows.Forms.Label lbl项目日期;
|
||||
private System.Windows.Forms.TextBox txt项目日期;
|
||||
private System.Windows.Forms.Label lbl项目名称;
|
||||
private System.Windows.Forms.TextBox txt项目名称;
|
||||
private System.Windows.Forms.Label lbl客户名称;
|
||||
private System.Windows.Forms.TextBox txt客户名称;
|
||||
private System.Windows.Forms.Button btn选择客户;
|
||||
private System.Windows.Forms.Label lbl客户地址;
|
||||
private System.Windows.Forms.TextBox txt客户地址;
|
||||
private System.Windows.Forms.Label lbl联系人;
|
||||
private System.Windows.Forms.TextBox txt联系人;
|
||||
private System.Windows.Forms.Label lbl联系电话;
|
||||
private System.Windows.Forms.TextBox txt联系电话;
|
||||
private System.Windows.Forms.Label lbl成套厂名称;
|
||||
private System.Windows.Forms.TextBox txt成套厂名称;
|
||||
private System.Windows.Forms.Label lbl成套厂地址;
|
||||
private System.Windows.Forms.TextBox txt成套厂地址;
|
||||
private System.Windows.Forms.Label lbl成套厂联系人;
|
||||
private System.Windows.Forms.TextBox txt成套厂联系人;
|
||||
private System.Windows.Forms.Label lbl成套厂联系电话;
|
||||
private System.Windows.Forms.TextBox txt成套厂联系电话;
|
||||
private System.Windows.Forms.Label lbl报出方式;
|
||||
private System.Windows.Forms.ComboBox cmb报出方式;
|
||||
private System.Windows.Forms.Label lbl箱柜模板;
|
||||
private System.Windows.Forms.ComboBox cmb箱柜模板;
|
||||
private System.Windows.Forms.Label lbl单台价格;
|
||||
private System.Windows.Forms.ComboBox cmb单台价格;
|
||||
private System.Windows.Forms.Label lbl项目总价;
|
||||
private System.Windows.Forms.Label lbl备注;
|
||||
private System.Windows.Forms.TextBox txt备注;
|
||||
private System.Windows.Forms.Button btn确定;
|
||||
private System.Windows.Forms.Button btn取消;
|
||||
private System.Windows.Forms.ComboBox comboBox1;
|
||||
private System.Windows.Forms.Label label1;
|
||||
private System.Windows.Forms.Label label2;
|
||||
private System.Windows.Forms.ComboBox comboBox2;
|
||||
private System.Windows.Forms.ComboBox cmb项目总价;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace MyApp
|
||||
{
|
||||
/// <summary>
|
||||
/// 新建项目 窗口(简化版,不依赖企业信息)。
|
||||
/// UI 布局见 新建项目.ui.cs,本文件只含交互逻辑。
|
||||
/// </summary>
|
||||
public partial class 新建项目 : Form
|
||||
{
|
||||
// 价格格式化选项(与主项目 价格格式化.cs 保持一致)
|
||||
private static readonly string[] 小数位选项 =
|
||||
{
|
||||
"保留整数", "保留1位小数", "保留2位小数", "保留3位小数",
|
||||
"保留4位小数", "保留5位小数", "保留6位小数"
|
||||
};
|
||||
private static readonly string[] 取整方式选项 =
|
||||
{
|
||||
"不取整", "四舍五入", "四舍五入到角", "四舍五入到元",
|
||||
"四舍五入到十元", "向上取整到元", "最近5元", "最近10元"
|
||||
};
|
||||
|
||||
private readonly string _dbPath;
|
||||
private readonly string _tableName;
|
||||
|
||||
public 新建项目() : this(null, null) { }
|
||||
|
||||
/// <summary>带数据库路径的构造函数(用于自动生成项目编号)。</summary>
|
||||
public 新建项目(string dbPath, string tableName)
|
||||
{
|
||||
InitializeComponent();
|
||||
_dbPath = dbPath;
|
||||
_tableName = tableName;
|
||||
InitPriceFormatCombos();
|
||||
this.Load += 新建项目_Load;
|
||||
}
|
||||
|
||||
private void InitPriceFormatCombos()
|
||||
{
|
||||
cmb单台价格.Items.Clear();
|
||||
cmb单台价格.Items.AddRange(小数位选项);
|
||||
cmb项目总价.Items.Clear();
|
||||
cmb项目总价.Items.AddRange(小数位选项);
|
||||
comboBox1.Items.Clear();
|
||||
comboBox1.Items.AddRange(取整方式选项);
|
||||
comboBox2.Items.Clear();
|
||||
comboBox2.Items.AddRange(取整方式选项);
|
||||
}
|
||||
|
||||
private void 新建项目_Load(object sender, EventArgs e)
|
||||
{
|
||||
FillAutoFields();
|
||||
}
|
||||
|
||||
/// <summary>自动填充项目日期(当天)和项目编号(yyyyMMdd+3位序号)。</summary>
|
||||
private void FillAutoFields()
|
||||
{
|
||||
DateTime today = DateTime.Today;
|
||||
txt项目日期.Text = today.ToString("yyyy-MM-dd");
|
||||
|
||||
string datePart = today.ToString("yyyyMMdd");
|
||||
int seq = GetNextSeqForDate(datePart);
|
||||
txt项目编号.Text = datePart + seq.ToString("000");
|
||||
txt项目编号.ReadOnly = true;
|
||||
txt项目编号.TabStop = false;
|
||||
txt项目编号.BackColor = System.Drawing.SystemColors.Control;
|
||||
}
|
||||
|
||||
/// <summary>查数据库当天最大序号+1,失败返回1。</summary>
|
||||
private int GetNextSeqForDate(string datePart)
|
||||
{
|
||||
if (string.IsNullOrEmpty(datePart) ||
|
||||
string.IsNullOrEmpty(_dbPath) ||
|
||||
string.IsNullOrEmpty(_tableName) ||
|
||||
!System.IO.File.Exists(_dbPath))
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
try
|
||||
{
|
||||
string connStr = "Data Source=" + _dbPath + ";Version=3;";
|
||||
using (var conn = new System.Data.SQLite.SQLiteConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
string sql =
|
||||
"SELECT MAX(CAST(SUBSTR(项目编号, 9) AS INTEGER)) " +
|
||||
"FROM " + _tableName + " " +
|
||||
"WHERE SUBSTR(项目编号, 1, 8) = '" + datePart + "';";
|
||||
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
|
||||
{
|
||||
object result = cmd.ExecuteScalar();
|
||||
if (result == null || result == DBNull.Value) return 1;
|
||||
int maxSeq;
|
||||
if (int.TryParse(result.ToString(), out maxSeq)) return maxSeq + 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { return 1; }
|
||||
}
|
||||
|
||||
/// <summary>收集对话框字段,以列名为键返回(供 Form3 写入数据库)。</summary>
|
||||
public Dictionary<string, string> GetInputValues()
|
||||
{
|
||||
var values = new Dictionary<string, string>();
|
||||
AddIfNotEmpty(values, "项目编号", txt项目编号.Text);
|
||||
AddIfNotEmpty(values, "项目日期", txt项目日期.Text);
|
||||
AddIfNotEmpty(values, "项目名称", txt项目名称.Text);
|
||||
AddIfNotEmpty(values, "客户名称", txt客户名称.Text);
|
||||
AddIfNotEmpty(values, "客户地址", txt客户地址.Text);
|
||||
AddIfNotEmpty(values, "联系人", txt联系人.Text);
|
||||
AddIfNotEmpty(values, "联系电话", txt联系电话.Text);
|
||||
AddIfNotEmpty(values, "备注", txt备注.Text);
|
||||
AddIfNotEmpty(values, "成套厂名称", txt成套厂名称.Text);
|
||||
AddIfNotEmpty(values, "成套厂地址", txt成套厂地址.Text);
|
||||
AddIfNotEmpty(values, "成套厂联系人", txt成套厂联系人.Text);
|
||||
AddIfNotEmpty(values, "成套厂联系电话", txt成套厂联系电话.Text);
|
||||
values["单台小数位"] = (cmb单台价格.SelectedItem ?? cmb单台价格.Text ?? "").ToString();
|
||||
values["项目小数位"] = (cmb项目总价.SelectedItem ?? cmb项目总价.Text ?? "").ToString();
|
||||
values["单台取整方式"] = (comboBox1.SelectedItem ?? comboBox1.Text ?? "").ToString();
|
||||
values["项目取整方式"] = (comboBox2.SelectedItem ?? comboBox2.Text ?? "").ToString();
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void AddIfNotEmpty(Dictionary<string, string> dict, string key, string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value)) dict[key] = value.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* pdfbt:PDF 扒图报价工具(由 cadbt 改造,CAD 查看器 → PDF 查看 + 框选 OCR)
|
||||
*/
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
internal sealed class Program
|
||||
{
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
// 自检模式:pdfbt.exe --selftest <pdf路径> [输出目录]
|
||||
if (args != null && args.Length >= 2 && args[0] == "--selftest")
|
||||
{
|
||||
return SelfTest.Run(args);
|
||||
}
|
||||
|
||||
// 单实例保护:重复启动时激活已有实例并退出
|
||||
bool createdNew;
|
||||
using (var mutex = new System.Threading.Mutex(true, "pdfbt_SingleInstance", out createdNew))
|
||||
{
|
||||
if (!createdNew)
|
||||
{
|
||||
MessageBox.Show("pdfbt 已在运行中。", "pdfbt",
|
||||
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return 0;
|
||||
}
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
Application.Run(new MainForm());
|
||||
GC.KeepAlive(mutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#region Using directives
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
#endregion
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("pdfbt")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("pdfbt")]
|
||||
[assembly: AssemblyCopyright("Copyright 2026")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// This sets the default COM visibility of types in the assembly to invisible.
|
||||
// If you need to expose a type to COM, use [ComVisible(true)] on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The assembly version has following format :
|
||||
//
|
||||
// Major.Minor.Build.Revision
|
||||
//
|
||||
// You can specify all the values or you can use the default the Revision and
|
||||
// Build Numbers by using the '*' as shown below:
|
||||
[assembly: AssemblyVersion("1.0.*")]
|
||||
@@ -0,0 +1,278 @@
|
||||
// ============================================================
|
||||
// QianfanOcrClient:百度千帆 PaddleOCR-VL-0.9B(版面解析)
|
||||
// · POST https://qianfan.baidubce.com/v2/ocr/paddleocr
|
||||
// Authorization: Bearer {QianfanApiKey}(千帆 ModelBuilder 的 API Key,永久有效)
|
||||
// {"model":"paddleocr-vl-0.9b","file":"<纯base64>","fileType":1}
|
||||
// · 响应 result.layoutParsingResults[0].prunedResult.parsing_res_list[]
|
||||
// 每块 {block_label, block_content, block_bbox[x1,y1,x2,y2](像素)}
|
||||
// → 文本块转 OcrItem;表格块逐行拆(与 GLM 处理一致)
|
||||
// · Key 存 baidu_ocr.json 的 qianfan_api_key(OCR设置窗口写)
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>百度千帆 PaddleOCR-VL 客户端(单实例)。</summary>
|
||||
internal sealed class QianfanOcrClient
|
||||
{
|
||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
||||
private string _apiKey;
|
||||
private const string BaseUrl = "https://qianfan.baidubce.com/v2/ocr/paddleocr";
|
||||
|
||||
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
|
||||
public event Action<string> StatusChanged;
|
||||
|
||||
public QianfanOcrClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
ServicePointManager.SecurityProtocol =
|
||||
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public bool IsConfigured
|
||||
{
|
||||
get { return !string.IsNullOrWhiteSpace(_apiKey); }
|
||||
}
|
||||
|
||||
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 qianfan_api_key 注入(热更新)。</summary>
|
||||
public void SetApiKey(string key)
|
||||
{
|
||||
_apiKey = key == null ? "" : key.Trim();
|
||||
}
|
||||
|
||||
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</summary>
|
||||
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
|
||||
return Task.FromResult(new List<OcrItem>());
|
||||
|
||||
if (!IsConfigured)
|
||||
{
|
||||
Notify("千帆 PaddleOCR-VL 未配置 API Key(OCR 设置窗口里填千帆 API Key)");
|
||||
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
|
||||
tcs0.SetException(new Exception(
|
||||
"千帆 PaddleOCR-VL 未配置 API Key。\r\n" +
|
||||
"请到百度智能云千帆平台(ModelBuilder)创建 API Key," +
|
||||
"在 OCR 设置窗口选择引擎\"百度 PaddleOCR-VL\"并填入后保存。"));
|
||||
return tcs0.Task;
|
||||
}
|
||||
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var result = RequestAndParse(crop, timeoutMs);
|
||||
Notify("PaddleOCR-VL·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
byte[] png;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
crop.Save(ms, ImageFormat.Png);
|
||||
png = ms.ToArray();
|
||||
}
|
||||
// 千帆要求:base64 直传(无 data URI 前缀),fileType=1 图像
|
||||
string body = _json.Serialize(new Dictionary<string, object>
|
||||
{
|
||||
{ "model", "paddleocr-vl-0.9b" },
|
||||
{ "file", Convert.ToBase64String(png) },
|
||||
{ "fileType", 1 }
|
||||
});
|
||||
|
||||
var req = (HttpWebRequest)WebRequest.Create(BaseUrl);
|
||||
req.Method = "POST";
|
||||
req.ContentType = "application/json";
|
||||
req.Headers.Add("Authorization", "Bearer " + _apiKey);
|
||||
req.Timeout = timeoutMs;
|
||||
req.ReadWriteTimeout = timeoutMs;
|
||||
req.Proxy = null;
|
||||
var bytes = Encoding.UTF8.GetBytes(body);
|
||||
req.ContentLength = bytes.Length;
|
||||
using (var rs = req.GetRequestStream())
|
||||
{
|
||||
rs.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
string respText;
|
||||
try
|
||||
{
|
||||
using (var resp = (HttpWebResponse)req.GetResponse())
|
||||
using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false)))
|
||||
{
|
||||
respText = sr.ReadToEnd();
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
string detail = "";
|
||||
try
|
||||
{
|
||||
if (wex.Response != null)
|
||||
using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false)))
|
||||
detail = sr.ReadToEnd();
|
||||
}
|
||||
catch { }
|
||||
string hint = wex.Response == null
|
||||
? ("网络不可达: " + wex.Message)
|
||||
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
|
||||
throw new Exception("PaddleOCR-VL 请求失败: " + hint);
|
||||
}
|
||||
|
||||
return ParseResponse(respText, crop.Width, crop.Height);
|
||||
}
|
||||
|
||||
// ---- 响应解析(行拆分/清理逻辑与 GLM 一致,复用其 shared 方法) ----
|
||||
|
||||
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
|
||||
{
|
||||
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
|
||||
if (msg == null) throw new Exception("千帆响应解析失败(空响应)");
|
||||
if (msg.ContainsKey("error"))
|
||||
{
|
||||
var err = msg["error"] as Dictionary<string, object>;
|
||||
string code = err != null && err.ContainsKey("code") ? Convert.ToString(err["code"]) : "?";
|
||||
string message = err != null && err.ContainsKey("message") ? Convert.ToString(err["message"]) : respText;
|
||||
throw new Exception("PaddleOCR-VL 错误 [" + code + "]: " + message);
|
||||
}
|
||||
|
||||
var items = new List<OcrItem>();
|
||||
// result.layoutParsingResults[0].prunedResult.parsing_res_list
|
||||
var result = msg.ContainsKey("result") ? msg["result"] as Dictionary<string, object> : null;
|
||||
var layouts = result != null && result.ContainsKey("layoutParsingResults")
|
||||
? result["layoutParsingResults"] as System.Collections.ArrayList : null;
|
||||
if (layouts == null || layouts.Count == 0)
|
||||
throw new Exception("PaddleOCR-VL 响应没有 layoutParsingResults(内容: " +
|
||||
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
|
||||
|
||||
var first = layouts[0] as Dictionary<string, object>;
|
||||
var pruned = first != null && first.ContainsKey("prunedResult")
|
||||
? first["prunedResult"] as Dictionary<string, object> : null;
|
||||
var blocks = pruned != null && pruned.ContainsKey("parsing_res_list")
|
||||
? pruned["parsing_res_list"] as System.Collections.ArrayList : null;
|
||||
if (blocks == null) return items;
|
||||
|
||||
foreach (Dictionary<string, object> b in blocks)
|
||||
{
|
||||
string label = b.ContainsKey("block_label") ? Convert.ToString(b["block_label"]) : "";
|
||||
string content = b.ContainsKey("block_content") ? Convert.ToString(b["block_content"]) : "";
|
||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||
|
||||
var bbox = b.ContainsKey("block_bbox") ? b["block_bbox"] as System.Collections.ArrayList : null;
|
||||
if (bbox == null || bbox.Count < 4) continue;
|
||||
|
||||
// block_bbox 像素坐标 [x1,y1,x2,y2];裁剪到图内
|
||||
float x1 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[0])));
|
||||
float y1 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[1])));
|
||||
float x2 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[2])));
|
||||
float y2 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[3])));
|
||||
if (x2 - x1 < 2 || y2 - y1 < 2) continue;
|
||||
|
||||
if (label == "table")
|
||||
{
|
||||
AddTableItems(items, content, x1, y1, x2, y2);
|
||||
continue;
|
||||
}
|
||||
if (label != "text" && label != "title" && label != "paragraph")
|
||||
continue; // 图片/公式/图表块跳过(title 也是文字,收)
|
||||
|
||||
content = GlmOcrClient.CleanHtmlShared(content);
|
||||
if (string.IsNullOrWhiteSpace(content)) continue;
|
||||
|
||||
string[] lines = content.Split(new[] { '\r', '\n' },
|
||||
StringSplitOptions.RemoveEmptyEntries);
|
||||
float lineH = (y2 - y1) / lines.Length;
|
||||
float pad = Math.Min(lineH * 0.15f, 3f);
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
string text = GlmOcrClient.CleanMarkdownShared(lines[i]).Trim();
|
||||
if (text.Length == 0) continue;
|
||||
float ty1 = y1 + lineH * i + pad;
|
||||
float ty2 = y1 + lineH * (i + 1) - pad;
|
||||
if (ty2 - ty1 < 2) { ty1 = y1 + lineH * i; ty2 = ty1 + Math.Max(2, lineH); }
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = text,
|
||||
Box = new[]
|
||||
{
|
||||
new PointF(x1, ty1),
|
||||
new PointF(x2, ty1),
|
||||
new PointF(x2, ty2),
|
||||
new PointF(x1, ty2)
|
||||
},
|
||||
Score = 1f
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex _trRegex =
|
||||
new System.Text.RegularExpressions.Regex("<tr[^>]*>(.*?)</tr>",
|
||||
System.Text.RegularExpressions.RegexOptions.Singleline |
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
private static readonly System.Text.RegularExpressions.Regex _tdRegex =
|
||||
new System.Text.RegularExpressions.Regex("<t[dh][^>]*>(.*?)</t[dh]>",
|
||||
System.Text.RegularExpressions.RegexOptions.Singleline |
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
|
||||
System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
|
||||
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
|
||||
private static void AddTableItems(List<OcrItem> items, string tableHtml,
|
||||
float x1, float y1, float x2, float y2)
|
||||
{
|
||||
var rows = new List<string>();
|
||||
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
|
||||
{
|
||||
var cells = new List<string>();
|
||||
foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value))
|
||||
{
|
||||
string cell = GlmOcrClient.CleanHtmlShared(td.Groups[1].Value).Trim();
|
||||
if (cell.Length > 0) cells.Add(cell);
|
||||
}
|
||||
if (cells.Count > 0) rows.Add(string.Join(" ", cells.ToArray()));
|
||||
}
|
||||
if (rows.Count == 0) return;
|
||||
float rowH = (y2 - y1) / rows.Count;
|
||||
float pad = Math.Min(rowH * 0.15f, 3f);
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
float ty1 = y1 + rowH * i + pad;
|
||||
float ty2 = y1 + rowH * (i + 1) - pad;
|
||||
if (ty2 - ty1 < 2) { ty1 = y1 + rowH * i; ty2 = ty1 + Math.Max(2, rowH); }
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = rows[i],
|
||||
Box = new[]
|
||||
{
|
||||
new PointF(x1, ty1),
|
||||
new PointF(x2, ty1),
|
||||
new PointF(x2, ty2),
|
||||
new PointF(x1, ty2)
|
||||
},
|
||||
Score = 1f
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void Notify(string s)
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// ============================================================
|
||||
// SelfTest:命令行自检(--selftest <pdf> [outdir])
|
||||
// 无 UI 验证:pdfium 渲染 → PNG 落盘;OCR 工作进程识别 → JSON 落盘
|
||||
// 用法:pdfbt.exe --selftest test.pdf .
|
||||
// 返回码:0 成功;1 失败
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
internal static class SelfTest
|
||||
{
|
||||
public static int Run(string[] args)
|
||||
{
|
||||
string pdf = args[1];
|
||||
string outDir = args.Length >= 3 ? args[2] : ".";
|
||||
var log = new StringBuilder();
|
||||
int exit = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// 1) pdfium 渲染
|
||||
string renderPath = Path.Combine(outDir, "selftest_render.png");
|
||||
Bitmap page;
|
||||
using (var doc = new PdfDocument())
|
||||
{
|
||||
doc.Open(pdf, null);
|
||||
log.AppendLine("[render] 打开成功: " + pdf + " 页数=" + doc.PageCount);
|
||||
page = doc.RenderPage(0, 1.5f);
|
||||
if (page == null) throw new Exception("渲染返回 null");
|
||||
}
|
||||
using (page)
|
||||
{
|
||||
page.Save(renderPath, ImageFormat.Png);
|
||||
log.AppendLine("[render] 已保存: " + renderPath +
|
||||
" (" + page.Width + "x" + page.Height + ")");
|
||||
|
||||
// 2) OCR(渲染图上半区)
|
||||
var crop = new Bitmap(page.Width, page.Height / 2,
|
||||
System.Drawing.Imaging.PixelFormat.Format24bppRgb);
|
||||
using (var g = Graphics.FromImage(crop))
|
||||
{
|
||||
g.DrawImage(page, new Rectangle(0, 0, crop.Width, crop.Height),
|
||||
new Rectangle(0, 0, page.Width, page.Height / 2), GraphicsUnit.Pixel);
|
||||
}
|
||||
|
||||
using (var ocr = new OcrClient())
|
||||
{
|
||||
var items = ocr.RecognizeAsync(crop, 180000).Result;
|
||||
var json = new StringBuilder();
|
||||
json.AppendLine("{");
|
||||
json.AppendLine(" \"count\": " + (items == null ? 0 : items.Count) + ",");
|
||||
json.AppendLine(" \"items\": [");
|
||||
if (items != null)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var it = items[i];
|
||||
var b = it.Bounds;
|
||||
json.AppendLine(" {\"text\": \"" +
|
||||
(it.Text ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") +
|
||||
"\", \"score\": " + it.Score.ToString("0.000") +
|
||||
", \"bounds\": [" + b.X.ToString("0") + "," + b.Y.ToString("0") +
|
||||
"," + b.Width.ToString("0") + "," + b.Height.ToString("0") + "]}" +
|
||||
(i < items.Count - 1 ? "," : ""));
|
||||
}
|
||||
}
|
||||
json.AppendLine(" ]");
|
||||
json.AppendLine("}");
|
||||
string ocrPath = Path.Combine(outDir, "selftest_ocr.json");
|
||||
File.WriteAllText(ocrPath, json.ToString(), new UTF8Encoding(false));
|
||||
log.AppendLine("[ocr] 识别 " + (items == null ? 0 : items.Count) +
|
||||
" 条,已保存: " + ocrPath);
|
||||
if (items != null)
|
||||
{
|
||||
foreach (var it in items)
|
||||
log.AppendLine(" - " + it.Text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.AppendLine("[FAIL] " + ex);
|
||||
exit = 1;
|
||||
}
|
||||
|
||||
string resultPath = Path.Combine(outDir, "selftest_result.txt");
|
||||
File.WriteAllText(resultPath, log.ToString(), new UTF8Encoding(false));
|
||||
Console.Out.WriteLine(log.ToString());
|
||||
return exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
// ============================================================
|
||||
// UapisOcrClient:UApiPro(uapis.cn) 通用文字识别(带位置)
|
||||
// · POST https://uapis.cn/api/v1/image/ocr (multipart/form-data)
|
||||
// X-API-Key: {key}(uapis.cn 控制台注册生成,免费积分计费 4积分/次,4QPS)
|
||||
// 字段:image_base64(纯base64或DataURI) + need_location="true"
|
||||
// · 响应 words_result[] { words, location{left,top,width,height},
|
||||
// vertexes_location[], score } → 转 OcrItem(坐标为发送图像素)
|
||||
// · Key 存 baidu_ocr.json 的 uapis_api_key(OCR设置窗口写)
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>UApiPro 通用 OCR 客户端(单实例)。</summary>
|
||||
internal sealed class UapisOcrClient
|
||||
{
|
||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
||||
private string _apiKey;
|
||||
private const string BaseUrl = "https://uapis.cn/api/v1/image/ocr";
|
||||
|
||||
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
|
||||
public event Action<string> StatusChanged;
|
||||
|
||||
public UapisOcrClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
ServicePointManager.SecurityProtocol =
|
||||
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public bool IsConfigured
|
||||
{
|
||||
get { return !string.IsNullOrWhiteSpace(_apiKey); }
|
||||
}
|
||||
|
||||
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 uapis_api_key 注入(热更新)。</summary>
|
||||
public void SetApiKey(string key)
|
||||
{
|
||||
_apiKey = key == null ? "" : key.Trim();
|
||||
}
|
||||
|
||||
/// <summary>识别一块位图 → 文本行列表(带位置)。multipart 上传。</summary>
|
||||
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
|
||||
return Task.FromResult(new List<OcrItem>());
|
||||
|
||||
if (!IsConfigured)
|
||||
{
|
||||
Notify("UApiPro OCR 未配置 API Key(OCR 设置窗口里填 UApiPro Key)");
|
||||
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
|
||||
tcs0.SetException(new Exception(
|
||||
"UApiPro(uapis.cn) OCR 未配置 API Key。\r\n" +
|
||||
"请到 uapis.cn/console 注册生成 API Key," +
|
||||
"在 OCR 设置窗口选择引擎\"UApiPro 通用OCR\"并填入后保存。"));
|
||||
return tcs0.Task;
|
||||
}
|
||||
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var result = RequestAndParse(crop, timeoutMs);
|
||||
Notify("UApiPro OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
byte[] png;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
crop.Save(ms, ImageFormat.Png);
|
||||
png = ms.ToArray();
|
||||
}
|
||||
string b64 = Convert.ToBase64String(png);
|
||||
|
||||
// multipart/form-data 手工拼装(net48 无 MultipartFormDataContent)
|
||||
string boundary = "----pdfbt" + Guid.NewGuid().ToString("N");
|
||||
var body = new MemoryStream();
|
||||
Action<string, string> field = (name, value) =>
|
||||
{
|
||||
byte[] part = Encoding.UTF8.GetBytes(
|
||||
"--" + boundary + "\r\nContent-Disposition: form-data; name=\"" +
|
||||
name + "\"\r\n\r\n" + value + "\r\n");
|
||||
body.Write(part, 0, part.Length);
|
||||
};
|
||||
field("image_base64", b64);
|
||||
field("need_location", "true");
|
||||
field("enable_cls", "true"); // 文字方向校正(歪扫图抗性)
|
||||
byte[] tail = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
|
||||
body.Write(tail, 0, tail.Length);
|
||||
|
||||
var req = (HttpWebRequest)WebRequest.Create(BaseUrl);
|
||||
req.Method = "POST";
|
||||
req.ContentType = "multipart/form-data; boundary=" + boundary;
|
||||
req.Headers.Add("X-API-Key", _apiKey);
|
||||
req.Timeout = timeoutMs;
|
||||
req.ReadWriteTimeout = timeoutMs;
|
||||
req.Proxy = null;
|
||||
byte[] payload = body.ToArray();
|
||||
req.ContentLength = payload.Length;
|
||||
using (var rs = req.GetRequestStream())
|
||||
{
|
||||
rs.Write(payload, 0, payload.Length);
|
||||
}
|
||||
|
||||
string respText;
|
||||
try
|
||||
{
|
||||
using (var resp = (HttpWebResponse)req.GetResponse())
|
||||
using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false)))
|
||||
{
|
||||
respText = sr.ReadToEnd();
|
||||
}
|
||||
}
|
||||
catch (WebException wex)
|
||||
{
|
||||
string detail = "";
|
||||
try
|
||||
{
|
||||
if (wex.Response != null)
|
||||
using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false)))
|
||||
detail = sr.ReadToEnd();
|
||||
}
|
||||
catch { }
|
||||
string hint = wex.Response == null
|
||||
? ("网络不可达: " + wex.Message)
|
||||
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
|
||||
throw new Exception("UApiPro OCR 请求失败: " + hint);
|
||||
}
|
||||
|
||||
return ParseResponse(respText, crop.Width, crop.Height);
|
||||
}
|
||||
|
||||
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
|
||||
{
|
||||
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
|
||||
if (msg == null) throw new Exception("UApiPro 响应解析失败(空响应)");
|
||||
|
||||
// 业务错误:HTTP 200 但 code 非 200/0;或 HTTP 错误体 {code,message}
|
||||
if (msg.ContainsKey("code"))
|
||||
{
|
||||
string code = Convert.ToString(msg["code"]);
|
||||
if (code != "200" && code != "0" && code != "success")
|
||||
{
|
||||
string m = msg.ContainsKey("message") ? Convert.ToString(msg["message"]) : respText;
|
||||
throw new Exception("UApiPro OCR 错误 [" + code + "]: " + m);
|
||||
}
|
||||
}
|
||||
|
||||
var items = new List<OcrItem>();
|
||||
var arr = msg.ContainsKey("words_result") ? msg["words_result"] as System.Collections.ArrayList : null;
|
||||
if (arr == null)
|
||||
throw new Exception("UApiPro 响应没有 words_result(内容: " +
|
||||
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
|
||||
|
||||
foreach (Dictionary<string, object> w in arr)
|
||||
{
|
||||
string text = w.ContainsKey("words") ? Convert.ToString(w["words"]) : "";
|
||||
if (string.IsNullOrWhiteSpace(text)) continue;
|
||||
|
||||
var loc = w.ContainsKey("location") ? w["location"] as Dictionary<string, object> : null;
|
||||
if (loc == null) continue;
|
||||
float l = LocF(loc, "left"), t = LocF(loc, "top");
|
||||
float wd = LocF(loc, "width"), h = LocF(loc, "height");
|
||||
// 裁剪到图内
|
||||
l = Math.Max(0, Math.Min(cropW, l));
|
||||
t = Math.Max(0, Math.Min(cropH, t));
|
||||
wd = Math.Max(2, Math.Min(cropW - l, wd));
|
||||
h = Math.Max(2, Math.Min(cropH - t, h));
|
||||
|
||||
float score = 1f;
|
||||
if (w.ContainsKey("score"))
|
||||
{
|
||||
float sv;
|
||||
if (float.TryParse(Convert.ToString(w["score"]), out sv)) score = sv;
|
||||
}
|
||||
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = text,
|
||||
Box = new[]
|
||||
{
|
||||
new PointF(l, t),
|
||||
new PointF(l + wd, t),
|
||||
new PointF(l + wd, t + h),
|
||||
new PointF(l, t + h)
|
||||
},
|
||||
Score = score
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static float LocF(Dictionary<string, object> d, string key)
|
||||
{
|
||||
if (d == null || !d.ContainsKey(key)) return 0f;
|
||||
float v;
|
||||
if (float.TryParse(Convert.ToString(d[key]), out v)) return v;
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private void Notify(string s)
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
// ============================================================
|
||||
// UmiOcrClient:Umi-OCR 本地 OCR 引擎(hiroi-sora/Umi-OCR,离线免费)
|
||||
// · 前提:用户在本机运行 Umi-OCR 主程序,全局设置里开启 HTTP 服务(默认开启,端口 1224)
|
||||
// · POST {BaseUrl}/api/ocr application/json:
|
||||
// { "base64": "<纯base64无前缀>", "options": { "data.format": "dict" } }
|
||||
// · 响应: code=100 成功;data=[{ text, score, box:[[x,y]×4 左上/右上/右下/左下], end }]
|
||||
// code=101 无文本(data 为原因字符串);其余 code 失败
|
||||
// · 地址存 baidu_ocr.json 的 umi_url(OCR设置窗口写,默认 http://127.0.0.1:1224)
|
||||
// · 本地引擎无需 API Key,IsConfigured 恒 true
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
/// <summary>Umi-OCR 本地引擎客户端(单实例)。</summary>
|
||||
internal sealed class UmiOcrClient
|
||||
{
|
||||
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
|
||||
private string _baseUrl = "http://127.0.0.1:1224";
|
||||
|
||||
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
|
||||
public event Action<string> StatusChanged;
|
||||
|
||||
public bool IsConfigured
|
||||
{
|
||||
get { return true; } // 本地引擎无需 Key
|
||||
}
|
||||
|
||||
/// <summary>服务地址由 OcrClient 从 baidu_ocr.json 的 umi_url 注入(热更新)。</summary>
|
||||
public void SetBaseUrl(string url)
|
||||
{
|
||||
url = (url == null ? "" : url.Trim());
|
||||
if (url.Length == 0) url = "http://127.0.0.1:1224";
|
||||
if (!url.StartsWith("http")) url = "http://" + url;
|
||||
_baseUrl = url.TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>识别一块位图 → 文本行列表(带位置)。JSON POST 到本机 Umi-OCR。</summary>
|
||||
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
|
||||
return Task.FromResult(new List<OcrItem>());
|
||||
|
||||
return Task.Factory.StartNew(() =>
|
||||
{
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var result = RequestAndParse(crop, timeoutMs);
|
||||
Notify("Umi-OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
|
||||
{
|
||||
// PNG base64(无 data: 前缀);PNG 保坐标精度,本地服务无 4MB 限制
|
||||
string b64;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
crop.Save(ms, ImageFormat.Png);
|
||||
b64 = Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
|
||||
string req = _json.Serialize(new Dictionary<string, object>
|
||||
{
|
||||
{ "base64", b64 },
|
||||
{ "options", new Dictionary<string, object> { { "data.format", "dict" } } }
|
||||
});
|
||||
|
||||
string body;
|
||||
using (var wc = new WebClient())
|
||||
{
|
||||
wc.Encoding = Encoding.UTF8;
|
||||
wc.Headers["Content-Type"] = "application/json";
|
||||
wc.Proxy = null; // 本地地址不走系统代理(配了代理会把 127.0.0.1 也代理走)
|
||||
body = wc.UploadString(_baseUrl + "/api/ocr", "POST", req);
|
||||
}
|
||||
|
||||
var resp = _json.Deserialize<Dictionary<string, object>>(body);
|
||||
if (resp == null || !resp.ContainsKey("code"))
|
||||
throw new Exception("Umi-OCR 返回解析失败");
|
||||
|
||||
int code;
|
||||
int.TryParse(Convert.ToString(resp["code"]), out code);
|
||||
if (code == 101)
|
||||
return new List<OcrItem>(); // 无文本,不算错误
|
||||
if (code != 100)
|
||||
throw new Exception("Umi-OCR 错误 code=" + code + ": " +
|
||||
Convert.ToString(resp.ContainsKey("data") ? resp["data"] : ""));
|
||||
|
||||
var items = new List<OcrItem>();
|
||||
var arr = resp.ContainsKey("data") ? resp["data"] as System.Collections.ArrayList : null;
|
||||
if (arr != null)
|
||||
{
|
||||
foreach (Dictionary<string, object> w in arr)
|
||||
{
|
||||
if (w == null) continue;
|
||||
var boxArr = w.ContainsKey("box") ? w["box"] as System.Collections.ArrayList : null;
|
||||
if (boxArr == null || boxArr.Count < 4) continue;
|
||||
var pts = new PointF[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var xy = boxArr[i] as System.Collections.ArrayList;
|
||||
float x, y;
|
||||
float.TryParse(Convert.ToString(xy != null && xy.Count > 0 ? xy[0] : "0"), out x);
|
||||
float.TryParse(Convert.ToString(xy != null && xy.Count > 1 ? xy[1] : "0"), out y);
|
||||
pts[i] = new PointF(x, y);
|
||||
}
|
||||
float score = 0.95f;
|
||||
if (w.ContainsKey("score"))
|
||||
{
|
||||
float sv;
|
||||
if (float.TryParse(Convert.ToString(w["score"]),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out sv)) score = sv;
|
||||
}
|
||||
items.Add(new OcrItem
|
||||
{
|
||||
Text = Convert.ToString(w.ContainsKey("text") ? w["text"] : ""),
|
||||
Box = pts, // box 即发送图像素坐标(顺时针四点),与 OcrItem 语义一致
|
||||
Score = score
|
||||
});
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private void Notify(string msg)
|
||||
{
|
||||
var h = StatusChanged;
|
||||
if (h != null) h(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// ============================================================
|
||||
// UmiOcrHost:Umi-OCR 本体(网盘 zip)下载/安装/启停/检测 管理器
|
||||
// · 本体 zip 直链(用户网盘,~179MB,zip 根直接是 Umi-OCR.exe 无顶层目录)
|
||||
// · 安装 = 下载 zip 到临时文件 → 解压到 exe 旁 Umi-OCR\ → 找到主程序验证 → 删 zip
|
||||
// · 启动 = Process.Start(主程序,Umi-OCR 启动即带 HTTP 服务 1224 端口) → 轮询端口就绪
|
||||
// · 停止 = 只停由本程序启动的实例(CloseMainWindow→超时 Kill),不碰用户自己开的
|
||||
// ============================================================
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
internal static class UmiOcrHost
|
||||
{
|
||||
/// <summary>本体 zip 直链(用户网盘直连,GET 才有内容,HEAD 会 404,不支持断点续传)。</summary>
|
||||
public const string DownloadUrl = "http://s333.odn.cc/s/845f64a239b665ec";
|
||||
|
||||
private const string InstallDirName = "Umi-OCR";
|
||||
private const int HttpPort = 1224;
|
||||
|
||||
/// <summary>由本程序启动的实例(停止时只关它,不碰用户自己开的)。</summary>
|
||||
private static Process _startedProc;
|
||||
|
||||
/// <summary>本体安装目录(exe 旁 Umi-OCR\)。</summary>
|
||||
public static string InstallDir
|
||||
{
|
||||
get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, InstallDirName); }
|
||||
}
|
||||
|
||||
/// <summary>找安装目录里的主程序(Umi-OCR.exe,大小写不敏感;找不到返回 null)。</summary>
|
||||
public static string FindExe()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(InstallDir)) return null;
|
||||
foreach (var f in Directory.GetFiles(InstallDir, "*.exe", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
string name = Path.GetFileName(f);
|
||||
if (name.Equals("Umi-OCR.exe", StringComparison.OrdinalIgnoreCase) ||
|
||||
name.Equals("Umi-OCR.exe", StringComparison.OrdinalIgnoreCase))
|
||||
return f;
|
||||
}
|
||||
// 目录名不完全一致时兜底:取第一个 exe
|
||||
string[] all = Directory.GetFiles(InstallDir, "*.exe", SearchOption.TopDirectoryOnly);
|
||||
return all.Length > 0 ? all[0] : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
/// <summary>本体是否已安装(目录里能找到主程序)。</summary>
|
||||
public static bool IsInstalled
|
||||
{
|
||||
get { return FindExe() != null; }
|
||||
}
|
||||
|
||||
/// <summary>HTTP 服务(1224 端口)是否在监听。</summary>
|
||||
public static bool IsHttpUp()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var c = new System.Net.Sockets.TcpClient())
|
||||
{
|
||||
var ar = c.BeginConnect("127.0.0.1", HttpPort, null, null);
|
||||
if (ar.AsyncWaitHandle.WaitOne(800))
|
||||
{
|
||||
c.EndConnect(ar);
|
||||
return true;
|
||||
}
|
||||
c.Close();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>三态:0=未安装,1=已安装未运行,2=运行中(HTTP 在线)。</summary>
|
||||
public static int Detect()
|
||||
{
|
||||
if (!IsInstalled) return 0;
|
||||
return IsHttpUp() ? 2 : 1;
|
||||
}
|
||||
|
||||
/// <summary>状态文字(设置窗口显示用)。</summary>
|
||||
public static string DetectText()
|
||||
{
|
||||
int st = Detect();
|
||||
if (st == 0) return "未检测到 OCR 本体(点\"下载 OCR 本体\"自动下载安装)";
|
||||
if (st == 1) return "已检测到 OCR 本体,服务未运行(识别时会自动启动)";
|
||||
return "OCR 本体运行中(HTTP " + HttpPort + " 端口在线)";
|
||||
}
|
||||
|
||||
/// <summary>下载 zip → 解压到 Umi-OCR\ → 验证主程序 → 删 zip。
|
||||
/// progress: 0~100 下载百分比;解压与收尾阶段回调 100。失败抛异常(内部已清理残留)。
|
||||
/// ★ 必须用 DownloadFileAsync:同步版 DownloadFile 不触发 DownloadProgressChanged
|
||||
/// (进度事件只在异步路径发,同步下载会一直停在 0%)。</summary>
|
||||
public static Task DownloadAndInstallAsync(Action<int> progress)
|
||||
{
|
||||
return Task.Factory.StartNew(delegate
|
||||
{
|
||||
string tmpZip = Path.Combine(Path.GetTempPath(), "Umi-OCR_" + Guid.NewGuid().ToString("N") + ".zip");
|
||||
try
|
||||
{
|
||||
if (progress != null) progress(0);
|
||||
// 异步下载包成同步等待(TaskCompletionSource),进度事件才会触发
|
||||
var dlTcs = new TaskCompletionSource<bool>();
|
||||
using (var wc = new WebClient())
|
||||
{
|
||||
wc.Proxy = null;
|
||||
wc.DownloadProgressChanged += (s, e) =>
|
||||
{
|
||||
if (progress != null) progress(e.ProgressPercentage);
|
||||
};
|
||||
wc.DownloadFileCompleted += (s, e) =>
|
||||
{
|
||||
if (e.Error != null) dlTcs.TrySetException(e.Error);
|
||||
else if (e.Cancelled) dlTcs.TrySetException(new Exception("下载已取消"));
|
||||
else dlTcs.TrySetResult(true);
|
||||
};
|
||||
wc.DownloadFileAsync(new Uri(DownloadUrl), tmpZip);
|
||||
}
|
||||
dlTcs.Task.GetAwaiter().GetResult(); // 等下载结束(异常原样抛出)
|
||||
if (progress != null) progress(100);
|
||||
|
||||
// 校验是 zip(PK 头)而不是错误页/JSON
|
||||
using (var fs = File.OpenRead(tmpZip))
|
||||
{
|
||||
int b0 = fs.ReadByte(), b1 = fs.ReadByte();
|
||||
if (b0 != 'P' || b1 != 'K')
|
||||
throw new Exception("下载的内容不是有效的压缩包(可能是网盘临时故障),请稍后重试");
|
||||
}
|
||||
|
||||
// 解压(覆盖);zip 根直接是 Umi-OCR.exe,目标目录即安装目录
|
||||
string dst = InstallDir;
|
||||
if (!Directory.Exists(dst)) Directory.CreateDirectory(dst);
|
||||
using (var archive = ZipFile.OpenRead(tmpZip))
|
||||
{
|
||||
foreach (var entry in archive.Entries)
|
||||
{
|
||||
string target = Path.GetFullPath(Path.Combine(dst, entry.FullName));
|
||||
if (!target.StartsWith(Path.GetFullPath(dst), StringComparison.OrdinalIgnoreCase))
|
||||
continue; // 防 zip 路径穿越
|
||||
if (string.IsNullOrEmpty(entry.Name))
|
||||
{
|
||||
if (!Directory.Exists(target)) Directory.CreateDirectory(target);
|
||||
continue;
|
||||
}
|
||||
string tdir = Path.GetDirectoryName(target);
|
||||
if (!string.IsNullOrEmpty(tdir) && !Directory.Exists(tdir)) Directory.CreateDirectory(tdir);
|
||||
if (File.Exists(target))
|
||||
{
|
||||
try { File.Delete(target); } catch { } // 被占用则跳过(旧文件同名多半一致)
|
||||
}
|
||||
entry.ExtractToFile(target, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (FindExe() == null)
|
||||
throw new Exception("解压完成但未找到 Umi-OCR 主程序,压缩包内容可能不完整");
|
||||
|
||||
try { File.Delete(tmpZip); } catch { } // 验证通过后删 zip
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { if (File.Exists(tmpZip)) File.Delete(tmpZip); } catch { }
|
||||
throw;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>确保本体已安装且 HTTP 服务在线:未安装抛错引导下载;未运行则静默启动(--hide 主窗口进托盘)并等端口就绪(≤15秒)。
|
||||
/// 返回 false=已安装但启动超时(调用方提示)。</summary>
|
||||
public static bool EnsureRunning()
|
||||
{
|
||||
if (!IsInstalled)
|
||||
throw new Exception("尚未安装 Umi-OCR 本体。\r\n请到 OCR 设置窗口选择 Umi-OCR 引擎,点\"下载 OCR 本体\"。");
|
||||
if (IsHttpUp()) return true;
|
||||
|
||||
string exe = FindExe();
|
||||
// --hide:官方参数,启动后主窗口隐藏到托盘,后台提供 HTTP 服务(用户看到零弹窗)
|
||||
_startedProc = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exe,
|
||||
Arguments = "--hide",
|
||||
WorkingDirectory = Path.GetDirectoryName(exe),
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WindowStyle = ProcessWindowStyle.Hidden
|
||||
});
|
||||
for (int i = 0; i < 30; i++) // 30 × 500ms = 最多 15 秒
|
||||
{
|
||||
if (IsHttpUp()) return true;
|
||||
System.Threading.Thread.Sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>停止由本程序启动的实例(PdfViewerWindow 关闭时调用;用户自己开的不动)。
|
||||
/// --hide 启动的窗口在托盘,CloseMainWindow 不可靠:先发官方 --quit 指令优雅退出,超时再 Kill。</summary>
|
||||
public static void StopIfStartedByUs()
|
||||
{
|
||||
var p = _startedProc;
|
||||
_startedProc = null;
|
||||
if (p == null) return;
|
||||
try
|
||||
{
|
||||
if (!p.HasExited)
|
||||
{
|
||||
// 官方退出指令:向已运行实例发 --quit(依赖其 HTTP 通道)
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = FindExe(),
|
||||
Arguments = "--quit",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
if (!p.WaitForExit(3000)) p.Kill();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
finally { try { p.Dispose(); } catch { } }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<appSettings>
|
||||
<!-- Python 解释器(已装 rapidocr + onnxruntime-directml) -->
|
||||
<add key="PythonExe" value="F:\PythonDev\python\python\python.exe" />
|
||||
<!-- OCR 工作进程脚本(相对 exe 目录) -->
|
||||
<add key="OcrWorkerScript" value="ocr_worker.py" />
|
||||
</appSettings>
|
||||
</configuration>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,60 @@
|
||||
-nologo
|
||||
-target:winexe
|
||||
-platform:x86
|
||||
-utf8output
|
||||
-optimize+
|
||||
-out:bin\Debug\pdfbt_new.exe
|
||||
-win32manifest:app.manifest
|
||||
-resource:MainForm.resx,pdfbt.MainForm.resources
|
||||
-resource:OcrSettingsForm.resx,pdfbt.OcrSettingsForm.resources
|
||||
-resource:Plugin\Form3.resx,pdfbt.Plugin.Form3.resources
|
||||
-resource:Plugin\新建项目.resx,pdfbt.Plugin.新建项目.resources
|
||||
-r:bin\Debug\Smart.CustomComponents.dll
|
||||
-r:bin\Debug\AdvancedDataGridView.dll
|
||||
-r:bin\Debug\WeifenLuo.WinFormsUI.Docking.dll
|
||||
-r:bin\Debug\WeifenLuo.WinFormsUI.Docking.ThemeVS2015.dll
|
||||
-r:bin\Debug\System.Data.SQLite.dll
|
||||
-r:bin\Debug\System.Memory.dll
|
||||
-r:bin\Debug\System.Numerics.Vectors.dll
|
||||
-r:bin\Debug\System.Resources.Extensions.dll
|
||||
-r:bin\Debug\System.Runtime.CompilerServices.Unsafe.dll
|
||||
-r:System.dll
|
||||
-r:System.Configuration.dll
|
||||
-r:System.Data.dll
|
||||
-r:System.Data.DataSetExtensions.dll
|
||||
-r:System.Xml.Linq.dll
|
||||
-r:System.IO.Compression.dll
|
||||
-r:System.IO.Compression.FileSystem.dll
|
||||
-r:bin\Debug\log4net.dll
|
||||
-r:netstandard.dll
|
||||
-r:System.Core.dll
|
||||
-r:System.Drawing.dll
|
||||
-r:System.Windows.Forms.dll
|
||||
-r:System.Xml.dll
|
||||
-r:System.Web.Extensions.dll
|
||||
-r:Microsoft.CSharp.dll
|
||||
MainForm.cs
|
||||
MainForm.Designer.cs
|
||||
MainForm.DockWindows.cs
|
||||
Program.cs
|
||||
SelfTest.cs
|
||||
PdfBridge.cs
|
||||
PdfiumNative.cs
|
||||
PdfDocument.cs
|
||||
OcrClient.cs
|
||||
GlmOcrClient.cs
|
||||
QianfanOcrClient.cs
|
||||
UapisOcrClient.cs
|
||||
UmiOcrClient.cs
|
||||
UmiOcrHost.cs
|
||||
OcrSettingsForm.cs
|
||||
OcrSettingsForm.Designer.cs
|
||||
PdfViewerWindow.cs
|
||||
Properties\AssemblyInfo.cs
|
||||
Plugin\Form3.cs
|
||||
Plugin\Form3.Designer.cs
|
||||
Plugin\PaTypes.cs
|
||||
Plugin\新建项目.cs
|
||||
Plugin\新建项目.Designer.cs
|
||||
DockWindows\QuoteWindow.cs
|
||||
DockWindows\QuoteWindow.Designer.cs
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,169 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" DefaultTargets="Build">
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}</ProjectGuid>
|
||||
<ProjectTypeGuids>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<RootNamespace>pdfbt</RootNamespace>
|
||||
<AssemblyName>pdfbt</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DebugSymbols>True</DebugSymbols>
|
||||
<DebugType>Full</DebugType>
|
||||
<Optimize>False</Optimize>
|
||||
<CheckForOverflowUnderflow>True</CheckForOverflowUnderflow>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DebugSymbols>False</DebugSymbols>
|
||||
<DebugType>None</DebugType>
|
||||
<Optimize>True</Optimize>
|
||||
<CheckForOverflowUnderflow>False</CheckForOverflowUnderflow>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Platform)' == 'AnyCPU' ">
|
||||
<Prefer32Bit>True</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp">
|
||||
<RequiredTargetFramework>4.0</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Data.DataSetExtensions">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Xml.Linq">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.Extensions">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.IO.Compression.FileSystem" />
|
||||
<Reference Include="Smart.CustomComponents, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ba044a4aa43e0dc4">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\Smart.CustomComponents.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="WeifenLuo.WinFormsUI.Docking">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\WeifenLuo.WinFormsUI.Docking.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="WeifenLuo.WinFormsUI.Docking.ThemeVS2015">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\WeifenLuo.WinFormsUI.Docking.ThemeVS2015.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Data.SQLite">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\System.Data.SQLite.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="log4net">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\log4net.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Resources.Extensions">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\System.Resources.Extensions.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Memory">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\System.Memory.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Runtime.CompilerServices.Unsafe">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\System.Runtime.CompilerServices.Unsafe.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Numerics.Vectors">
|
||||
<HintPath>C:\Program Files (x86)\SharpDevelop\5.2\bin\System.Numerics.Vectors.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="AdvancedDataGridView">
|
||||
<HintPath>libs\AdvancedDataGridView.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<!-- System.Memory 等 facade 库触发 CS0012,需 netstandard 契约 -->
|
||||
<Reference Include="netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51">
|
||||
<HintPath>C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.8\Facades\netstandard.dll</HintPath>
|
||||
<Private>False</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="MainForm.cs" />
|
||||
<Compile Include="MainForm.Designer.cs">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="MainForm.DockWindows.cs">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="SelfTest.cs" />
|
||||
<Compile Include="PdfBridge.cs" />
|
||||
<Compile Include="PdfiumNative.cs" />
|
||||
<Compile Include="PdfDocument.cs" />
|
||||
<Compile Include="OcrClient.cs" />
|
||||
<Compile Include="GlmOcrClient.cs" />
|
||||
<Compile Include="QianfanOcrClient.cs" />
|
||||
<Compile Include="UapisOcrClient.cs" />
|
||||
<Compile Include="UmiOcrClient.cs" />
|
||||
<Compile Include="UmiOcrHost.cs" />
|
||||
<Compile Include="OcrSettingsForm.cs" />
|
||||
<Compile Include="OcrSettingsForm.Designer.cs" />
|
||||
<Compile Include="PdfViewerWindow.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Plugin\Form3.cs" />
|
||||
<Compile Include="Plugin\Form3.Designer.cs">
|
||||
<DependentUpon>Form3.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Plugin\PaTypes.cs" />
|
||||
<Compile Include="Plugin\新建项目.cs" />
|
||||
<Compile Include="Plugin\新建项目.Designer.cs">
|
||||
<DependentUpon>新建项目.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="DockWindows\QuoteWindow.cs" />
|
||||
<Compile Include="DockWindows\QuoteWindow.Designer.cs">
|
||||
<DependentUpon>QuoteWindow.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.config" />
|
||||
<None Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- x86 pdfium(pypdfium2 win32 wheel 提取,与 SQLite x86 一致) -->
|
||||
<Content Include="libs\pdfium\pdfium.dll">
|
||||
<Link>pdfium.dll</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<!-- Python OCR 工作进程脚本(rapidocr + DirectML GPU 加速) -->
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="MainForm.resx">
|
||||
<DependentUpon>MainForm.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="OcrSettingsForm.resx">
|
||||
<DependentUpon>OcrSettingsForm.Designer.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Plugin\Form3.resx">
|
||||
<DependentUpon>Form3.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Plugin\新建项目.resx">
|
||||
<DependentUpon>新建项目.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 2012
|
||||
# SharpDevelop 5.2
|
||||
VisualStudioVersion = 12.0.20827.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "pdfbt", "pdfbt.csproj", "{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7C2A9D4E-1F58-4B6A-9E77-3A5B10D9C8F1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,31 @@
|
||||
// 复现 OCR 设置窗报错,把完整异常写到文件
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace pdfbt
|
||||
{
|
||||
static class ShowErr
|
||||
{
|
||||
[STAThread]
|
||||
static void Main()
|
||||
{
|
||||
try
|
||||
{
|
||||
var f = new OcrSettingsForm(null);
|
||||
f.Show();
|
||||
Application.DoEvents();
|
||||
FileLog("窗口创建成功,无异常");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FileLog("EX: " + ex);
|
||||
var inner = ex.InnerException;
|
||||
while (inner != null) { FileLog("INNER: " + inner); inner = inner.InnerException; }
|
||||
}
|
||||
}
|
||||
static void FileLog(string s)
|
||||
{
|
||||
System.IO.File.AppendAllText(@"C:\Users\099978\Documents\SharpDevelop Projects\pdfbt\showerr.log", s + "\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user