NetOfd 是 OFDRW (OFD Reader & Writer) v2.3.8 的 .NET 跨平台移植版本, 实现中国国家标准 GB/T 33190-2016 电子文件存储与交换格式版式文档(OFD)的完整读写功能, 覆盖数字签名(GB/T 35275 / SES_V1-V5)、国密应用(GM/T 0099-2020)、OFD-A 归档(GB/T 42133-2022)等配套标准。
- DrawParam 样式级联:按 ParentID 链解析继承(线宽/颜色/填充/透明度),渲染端与 Java 版一致
- 裁剪链(Clips):模型解析、Graphics2D 记录、渲染器 ClipPath 全链路支持
- 批量编辑会话
OfdEditSession:注释/版本/水印等多次编辑一次落盘;OfdDocEditor支持并行导出 - 真实语料护栏:67 个 Java 测试文档全量 打开/渲染/提取 冒烟测试(100% 通过)
- 文字装饰:下划线/删除线/加粗/斜体/段落对齐完整落盘
NetOfd.Emr 模块提供 GB/T 48666—2026《电子病历版式文档技术要求》 的落地能力:
作为库 API 生成符合 OFD-H 要求的版式文档,并在其中嵌入机读的结构化诊疗数据。
samples/OfdHWriter / samples/OfdHExporter 两个命令行工具是其封装。
| 标准 | 含义 | 在 NetOfd 中的落地 |
|---|---|---|
| GB/T 48666—2026 | 电子病历版式文档技术要求 | 主标准:DocType 标识、包结构、元数据、标引、附件、版本、加注、签名、防夹带 |
| GB/T 33190—2016 | 电子文件存储与交换格式 版式文档 | OFD 基础格式(Doc_0 单文档、ofd: 命名空间前缀、ZIP 线性化) |
| WS/T 500.32—2016 | 电子病历共享文档规范 第 32 部分:住院病案首页 | HL7 CDA R2 ClinicalDocument 构造与解析(Wst500Builder) |
| WS 445.10—2014 | 电子病历基本数据集 第 10 部分:住院病案首页 | 56 个 DE 码数据元(Wst500Codes)与结构化导出 |
| WS/T 482—2016 | 卫生信息共享文档编制规范 | 共享文档体结构约束 |
| GB/T 35275 | 信息安全技术 数字签名格式规范 | SM2withSM3 国密签名 / 验签(OfdSigner) |
| GM/T 0099—2020 | 电子文档安全技术 签名存证与防抵赖 | §7.4.3 防夹带 / 完整性保护(OfdIntegrity) |
引用的代码表:GB/T 2261.1(生理性别)、GB/T 2261.2(婚姻状况)、GB 3304(民族类别)、 ISO 3166-1(国籍)、ICD-10(疾病分类)、ICD-9-CM-3(手术操作)、LOINC(章节编码)。
src/NetOfd.Emr/OfdHDocumentGenerator.cs 顶部注释逐条记录了生成流程与标准条款的对应关系:
| 条款 | 要求 | 实现 |
|---|---|---|
| 6.1.2 | 单文档承载(Doc_0,OFD 格式) |
OFDDoc 单文档 |
| 6.1.3 | ZIP 6.2 + Deflate、线性化(OFDEntries.xml 索引) |
RepackLinearized |
| 6.2 | 包内规范目录结构(表 1) | NormalizeStructure |
| 7.1 a) | DocType="OFD-H" |
OFRoot.OfdDocType |
| 7.1 c) | 元数据 DocInfo(患者 / 责任者 / 时间 / 关键词) | ConfigureDocInfo |
| 7.2 b) | 字型数据嵌入(仿宋 FontFile) | EmbedFonts |
| 7.2 c) | 图像资源嵌入(DocumentRes + Res/) |
Img 元素 |
| 7.3 c) | 结构化数据以自定义标引存储 | OfdTagger.AddCustomTag ×2 |
| 7.4 | 附件(Attachs/Attachments.xml + Attach_N.png) |
OfdAttacher + 规范化 |
| 7.5 | 机读信息(文件头 / 元数据 / 标识信息,UTF-8) | 机读信息 XML 标引 |
| 7.6 | 版本快照(历史版本全量保留) | OfdVersioner |
| 3.12 / 5.3.2 e) | 加注记录(审核批注) | OfdAnnotator |
| 6.3 / 8 c) d) e) | SM2 + SM3 国密签名,保护范围除注释列表、签名列表外全部内容 | OfdSigner + GBT35275DSContainer |
| GM/T 0099 §7.4.3 | 防夹带 / 完整性保护 | OfdIntegrity + GmProtectSigner |
using NetOfd.Emr;
var result = OfdHDocumentGenerator.Generate(new OfdHOptions
{
OutputPath = "病历.ofd",
CaseData = myCaseData, // 传入 EmrCaseData;为 null 时使用内置演示数据
// FontPath = "/path/to/chinese.ttf", // 不指定则按平台自动探测中文字型
});
if (!result.Success)
{
Console.Error.WriteLine(result.Error);
return;
}
Console.WriteLine($"{result.PageCount} 页 / {result.SizeBytes} 字节 / 签名 {result.SignatureCount} 个");
Console.WriteLine($"验签全部通过: {result.AllSignaturesValid},完整性: {result.IntegrityVerified}");产出一份 3 页 A4 纵向(边距 20mm)OFD-H 版式文档:
| 页 | 内容 |
|---|---|
| 1 | 住院病案首页(基本信息 6 行 / 就诊信息 6 行 / 诊断 2 行) |
| 2 | 入院记录(一般项目、主诉、现病史、既往史、个人史 + 家族史、体格检查) |
| 3 | 辅助检查(心电图文字 + 图像 + 图注)、初步诊断与诊疗计划、医师签名、日期 |
文档同时包含:嵌入中文字型、心电图图像资源、内部附件、质控审核加注、版本快照、 机读信息标引 + WS/T 500 共享文档标引、SM2withSM3 国密签名、GM/T 0099 防夹带保护。
OfdHOptions 常用参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
OutputPath |
必填 | 输出 .ofd 路径 |
CaseData |
内置演示数据 | 病历数据(EmrCaseData) |
SkipSignature |
false |
跳过签名与防夹带(仅调试结构用) |
DocumentId |
自动生成 GUID | 文档标识 DocID;传入固定值可获得可复现输出 |
FontPath |
按平台自动探测 | 待嵌入的中文字型文件 |
SkipFontEmbeddingWhenMissing |
true |
找不到字型时记录警告并跳过(设为 false 则抛异常) |
SigningTime |
当前时间 | 签名时间 |
Log |
Console.Out |
日志输出目标 |
dotnet run --project samples/OfdHWriter -- <out.ofd> [--work <工作目录>] [--font <字型路径>] [--no-sign] [--no-preview]退出码:0 成功且签名自检通过 / 1 参数错误 / 2 生成异常 / 3 签名自检失败。
| 类 | 职责 |
|---|---|
OfdHDocumentGenerator |
OFD-H 版式文档生成器(本节上文) |
OfdHOptions / OfdHResult |
生成参数与结果对象 |
EmrCaseData |
病历数据模型(患者、就诊、诊断、医嘱等) |
EmrStructuredData |
版式页面与机读数据的桥梁:同一份字段既渲染进 OFD-H 版式,又以标准 DE 码写入结构化标引 |
Wst500Builder |
把 EmrStructuredData 序列化为 WS/T 500.32 共享文档(HL7 CDA R2 ClinicalDocument) |
Wst500Codes |
编码常量字典:13 个章节 LOINC 码、56 个数据元 DE 码、11 个 OID 根、11 个值域代码 |
EmrStructuredExporter |
从 OFD-H 读回自定义标引并还原为标准结构化数据(JSON / 文本) |
using NetOfd.Emr;
var data = new EmrStructuredData
{
PatientName = "张建国",
SexCode = Wst500Codes.SexMale, // "1"
BirthDate = "19651120",
};
data.Diagnoses.Add(new EmrDiagnosis { Name = "不稳定型心绞痛", Code = "I20.0" });
// 构造 WS/T 500.32 共享文档(可作为 OFD-H 自定义标引的载荷)
byte[] sharedDoc = Wst500Builder.BuildBytes(data);
// 从 OFD-H 反向导出结构化数据
var result = EmrStructuredExporter.Export("病历.ofd");
Console.WriteLine(EmrStructuredExporter.ToJson(result));Wst500Builder 生成的 ClinicalDocument 覆盖 10 个章节(LOINC 编码),
entry 内嵌于 section 之下,观测值分 PQ(数值 + 单位)/ CD(编码值)/ ST(文本)三类。
dotnet run --project samples/OfdHExporter -- <in.ofd> [--out <输出目录>] [--format json|xml|text|all]| 产物 | 内容 |
|---|---|
<base>_结构化数据.json |
WS 445.10 扁平数据元(DE 码 → 值)+ 诊断数组 |
<base>_WS_T500共享文档.xml |
WS/T 500.32 共享文档原文 |
<base>_结构化数据.txt |
可读文本汇总 |
退出码:0 成功 / 1 参数错误或输入不存在 / 2 导出异常 / 3 未找到结构化数据标引。
OFD-H 相关自动化测试位于 tests/NetOfd.Tests/Emr/:
| 测试类 | 用例数 | 覆盖内容 |
|---|---|---|
OfdHDocumentGeneratorTest |
18 | 生成全流程端到端:3 页结构、DocType、DocID、包内目录结构(表 1)、线性化顺序、字型嵌入、版本快照、自定义标引 ×2、机读信息三要素、结构化数据反向导出、CDA 结构、附件字节一致、加注、签名验签、防夹带完整性、三页文本提取 |
OfdHGeneratorOptionTest |
5 | 跳过签名、不存在字型路径(跳过 / 抛异常两种策略)、自定义病历数据、自动 DocID |
Wst500SharedDocumentTest |
12 | WS/T 500.32 共享文档常量自洽性、CDA 结构约束、标引写入读回闭环 |
诚实列出当前实现的局限,便于评估可用范围:
- 章节覆盖 10/13:已定义但未实现的 3 个章节为转科记录(
28570-0)、出院诊断(56846-4)、行政管理(32473-6)。 - 数据元覆盖 33/56:56 个 DE 码常量中约 23 个尚未写入共享文档(用药、血型、出生体重、尸检标志、病理号、外因等)。
- 部分字段只入版式、不入机读:
EmrStructuredData中的就诊类字段(年龄、住院次数、入出院时间、入院途径、入出院科室、病区、付费方式、责任护士、编码医师等)目前仅出现在版式页面,未进入结构化标引。 - 仅覆盖 1 个共享文档 part:当前只实现 WS/T 500.32(住院病案首页),WS/T 500 系列其余 part 未涉及。
- 字型嵌入依赖系统中文字体:未显式指定
FontPath时按平台探测仿宋/中文字体;探不到时按选项记录警告并跳过(此时不满足第 7.2 b 条),生产环境应显式提供字型文件。
netofd/
├── src/
│ ├── NetOfd.Core/ # 核心:OFD XML 元素体系 + 基础类型(ST_*/CT_*)
│ ├── NetOfd.Emr/ # OFD-H 电子病历(GB/T 48666—2026):版式文档生成 + WS/T 500 共享文档 + 标准导出
│ ├── NetOfd.Pkg/ # 虚拟容器 + ZIP 打包
│ ├── NetOfd.Font/ # 字体管理与度量
│ ├── NetOfd.Reader/ # OFD 解析器 / 文本提取 / 关键词定位
│ ├── NetOfd.Layout/ # 布局引擎 + 编辑器(注释/附件/自定义标/版本)
│ ├── NetOfd.Graphics2D/ # java.awt 风格绘图接口
│ ├── NetOfd.Converter/ # OFD → PNG/SVG/HTML 渲染
│ ├── NetOfd.Sign/ # 数字签名/电子签章(SES_V1/V4/V5,GB/T 35275)
│ ├── NetOfd.Crypto/ # 国密文档加密解密(SM4/KDF,口令与证书方案)
│ ├── NetOfd.Tool/ # 文档工具(合并/混合/删页)
│ ├── NetOfd.Archive/ # OFD-A 归档合规检查与转换
│ ├── NetOfd.Full/ # 全模块聚合
│ └── NetOfd/ # 统一入口(元包)
├── tests/
│ └── NetOfd.Tests/ # 950+ 单元/集成/性能测试
├── samples/ # 示例项目(写入/签名/渲染转换/OFD-H 生成/结构化导出)
│ └── OfdHWriter/ # GB/T 48666—2026 电子病历 OFD-H 生成器
└── Directory.Build.props # 统一构建属性
using NetOfd.Layout;
using NetOfd.Layout.Element;
using var doc = new OFDDoc("output.ofd");
doc.AddPage();
doc.Add(new Paragraph().SetWidth(100)
.Add(new Span("你好,OFD 世界!").SetFontSize(5d).SetBold()));using NetOfd.Reader;
using NetOfd.Converter; // ContentExtractor 所在命名空间以实际为准
using var reader = new OFDReader("input.ofd");
var pages = reader.GetPages().ToList();
foreach (var text in ContentExtractor.ExtractAllPagesText(pages))
Console.WriteLine(text);using NetOfd.Layout.Edit;
// 方式一:逐次编辑(每次返回新字节)
var edited = OfdAnnotator.AddAnnotation(ofdBytes, 1,
new Annotation(10, 20, 40, 16, AnnotType.Path, drawer));
edited = OfdAttacher.AddAttachment(edited, new Attachment("合同扫描件", "scan.pdf"));
edited = OfdTagger.AddCustomTag(edited, "urn:example:keyword", tagBytes);
OfdVersioner.AddVersion(edited, "定稿", out _); // 留存版本
var old = OfdVersioner.ExtractVersion(edited, "v1"); // 还原历史版本
// 方式二:批量编辑会话(多次操作单次落盘,大批量场景快数倍)
using (var session = OfdEditSession.Open(ofdBytes))
{
for (int p = 1; p <= session.PageCount; p++)
session.AddAnnotation(p, annotation);
session.AddVersion("批注定稿");
ofdBytes = session.Save(); // 落盘必须显式调用
}VirtualPage 适合坐标自算的固定版式(表单、报表、封面):内部元素全部绝对定位,不会自动分页。
using NetOfd.Layout;
using NetOfd.Layout.Element;
var vPage = new VirtualPage(PageLayout.A4());
vPage.Add(new Div()
.SetPosition(Position.Absolute) // 必须绝对定位
.SetXY(20, 30) // 必须给出 X / Y
.SetWidth(170) // 必须给出 Width
.SetHeight(10)
.SetBackgroundColor(new CT_Color { Value = "0,0,255" }));
doc.AddVPage(vPage); // 仅入队,Close() 时才落成真实页面
doc.Close();- 插入到指定页码(v1.3.1 新增):
vPage.SetPageNum(2)会在渲染时把该页插入到第 2 页的位置; 不设置则追加到末尾。渲染后vPage.PageNum会回填为该页的最终页码,便于调用方查询。 物理目录Page_N仍按追加顺序分配,文档页序由Document.xml的<Page>列表决定, 因此按页码读取(OFDReader.GetPageText(2))始终正确。 - 母版/底纹复用:
vPage.AddTemplate(tpl)复用固定底纹(生成独立Tpl_M/Content.xml,以<Template>引用叠加在正文之下)。 - 图层归属:
Div.Layer决定元素落在前景层 / 正文层 / 背景层(默认正文层Body)。 ⚠️ 造多页请用VirtualPage或PageAreaFiller:流式Add与AddPage交错会让前面的页空白 (流式内容统一从最新页开始排布)。
using NetOfd.Sign;
var signed = OfdSigner.Sign(ofdBytes, signerKeyPair, signerCert,
"2026-08-23 10:00:00", seal: sealAsn1, isSeal: true, version: 4);
var result = OfdValidator.Verify(signed);
Console.WriteLine(result.AllValid ? "全部有效" : result.Error);using NetOfd.Converter;
using var exporter = new ImageExporter("input.ofd", outDir, "png", ppm: 15);
var pngPaths = exporter.Export(); // 每页一个 PNGdotnet run --project tests/NetOfd.TestConsole -- info <file.ofd>
dotnet run --project tests/NetOfd.TestConsole -- verify <file.ofd>
dotnet run --project tests/NetOfd.TestConsole -- text <file.ofd>
dotnet run --project tests/NetOfd.TestConsole -- render <file.ofd> <outDir> [ppm]
dotnet run --project tests/NetOfd.TestConsole -- merge <out.ofd> <in1.ofd> <in2.ofd> ...
dotnet run --project tests/NetOfd.TestConsole -- sample <out.ofd>
dotnet run --project tests/NetOfd.TestConsole -- sign <in.ofd> <out.ofd> --p12 <cert.p12> --pwd <密码> [--seal <seal.der>] [--version 1|4|5]
dotnet run --project tests/NetOfd.TestConsole -- protect <in.ofd> <out.ofd> --user <用户名> --pwd <口令>
dotnet run --project tests/NetOfd.TestConsole -- decrypt <in.ofd> <out.ofd> [--user <用户名>] --pwd <口令>
dotnet run --project tests/NetOfd.TestConsole -- ofda-check <file.ofd>
dotnet run --project tests/NetOfd.TestConsole -- ofda-convert <in.ofd> <out.ofd> [--user <u> --pwd <p>]samples/OfdHWriter 是 NetOfd.Emr.OfdHDocumentGenerator 的命令行封装,输出含 3 页版式内容、
嵌入中文字型、心电图附件、质控审核加注、版本快照、自定义标引(机读信息 + 结构化诊疗数据)、
SM2withSM3 国密签名与 GM/T 0099 防夹带保护:
dotnet run --project samples/OfdHWriter -- <out.ofd> [--work <工作目录>] [--font <字型路径>] [--no-sign] [--no-preview]退出码:0 成功且签名自检通过 / 1 参数错误 / 2 生成异常 / 3 签名自检失败。
完整条款对照、库 API、结构化数据与测试覆盖见 OFD-H 电子病历支持。
- 跨平台: 支持 Windows、Linux、macOS(.NET 10;Linux 渲染需安装 CJK 字体)
- 编辑器架构: zip 展开为内存映射 + XML 直改,链式调用安全(对象 ID 单调递增)
- 版本管理: 文档级快照(Doc_0/V_N)与历史版本提取
- 国密: SM2/SM3/SM4 全覆盖,兼容 BouncyCastle 与系统密码库互操作验证
- 健壮性: 损坏输入有语义明确的异常或结构化错误结果(950+ 测试保障)
| Java (OFDRW) | .NET (NetOfd) |
|---|---|
dom4j Element |
System.Xml.Linq.XElement |
| zip4j | System.IO.Compression.ZipArchive |
| BouncyCastle SM2/SM3/SM4 | BouncyCastle.Cryptography |
java.awt.Graphics2D |
SkiaSharp(渲染)/ NetOfd.Graphics2D 接口层 |
| Maven multi-module | .NET Solution + csproj |
| 包 | 用途 |
|---|---|
BouncyCastle.Cryptography |
SM2/SM3/SM4 国密算法 |
SkiaSharp |
跨平台渲染(Converter/Graphics2D) |
各版本的发布说明(每个版本一份中文、一份英文)见 docs/releases/:
| 版本 | 中文 | English |
|---|---|---|
| v1.3.4(当前) | v1.3.4.md | v1.3.4.en.md |
| v1.3.3 | v1.3.3.md | v1.3.3.en.md |
| v1.3.2 | v1.3.2.md | v1.3.2.en.md |
| v1.3.1 | v1.3.1.md | v1.3.1.en.md |
| v1.3.0 | v1.3.0.md | v1.3.0.en.md |
本项目采用双协议:
| 范围 | 协议 | 文件 |
|---|---|---|
NetOfd 自有代码(如 NetOfd.Emr 等非移植模块) |
MIT License | LICENSE |
| 自 OFDRW 移植而来的代码 | Apache License 2.0 | LICENSE-Apache-2.0 |
各 NuGet 包的 PackageLicenseExpression 与此保持一致:
| 包 | 声明 |
|---|---|
NetOfd.Emr |
MIT |
NetOfd.Core / Pkg / Font / Reader / Layout / Graphics2D / Converter / Sign / Crypto / Tool / Archive |
Apache-2.0 |
NetOfd / NetOfd.Full(聚合包) |
Apache-2.0 OR MIT |
NetOfd 是 OFDRW(Copyright (c) 2020 Quan guanyu,Apache-2.0) 的 .NET 移植版本。根据 Apache-2.0 §4,移植而来的部分保留原协议、声明修改并保留版权声明, 详见 NOTICE。
根目录
LICENSE为 MIT 协议标准原文(便于 GitHub 自动识别协议标签), 上述协议适用范围划分以NOTICE与本表为准。
本项目是独立实现,与 OFDRW 项目无隶属或背书关系。