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

This commit is contained in:
dqb-dev
2026-10-01 22:15:22 +08:00
commit 1e65792b09
499 changed files with 204579 additions and 0 deletions
@@ -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
View File
@@ -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
}
}
+26
View File
@@ -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();
}
}
}
+355
View File
@@ -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("&nbsp;", " ").Replace("&amp;", "&")
.Replace("&lt;", "<").Replace("&gt;", ">")
.Replace("&quot;", "\"").Replace("&apos;", "'");
}
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);
}
}
}
+71
View File
@@ -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;
}
}
+28
View File
@@ -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);
}
}
}
+82
View File
@@ -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;
}
}
}
}
+120
View File
@@ -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>
+417
View File
@@ -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() { }
}
}
+303
View File
@@ -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取消;
}
}
+231
View File
@@ -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);
}
}
}
}
+120
View File
@@ -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>
+148
View File
@@ -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();
}
}
}
+160
View File
@@ -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
+228
View File
@@ -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);
}
}
}
+1321
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+120
View File
@@ -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>
+29
View File
@@ -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 = "";
}
}
+609
View File
@@ -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项目总价;
}
}
+132
View File
@@ -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();
}
}
}
+120
View File
@@ -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>
+38
View File
@@ -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;
}
}
}
+31
View File
@@ -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.*")]
+278
View File
@@ -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);
}
}
}
+99
View File
@@ -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;
}
}
}
+221
View File
@@ -0,0 +1,221 @@
// ============================================================
// UapisOcrClient:UApiPro(uapis.cn) 通用文字识别(带位置)
// · POST https://uapis.cn/api/v1/image/ocr (multipart/form-data)
// X-API-Key: {key}(uapis.cn 控制台注册生成,免费积分计费 4积分/次,4QPS)
// 字段:image_base64(纯base64或DataURI) + need_location="true"
// · 响应 words_result[] { words, location{left,top,width,height},
// vertexes_location[], score } → 转 OcrItem(坐标为发送图像素)
// · Key 存 baidu_ocr.json 的 uapis_api_key(OCR设置窗口写)
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 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);
}
}
}
+141
View File
@@ -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);
}
}
}
+232
View File
@@ -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 { } }
}
}
}
+12
View File
@@ -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>
+8
View File
@@ -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>
+60
View File
@@ -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.
+169
View File
@@ -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>
+20
View File
@@ -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
View File
+31
View File
@@ -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");
}
}
}