前言
OFD是国家标准版式文档格式,于2016年生效。OFD文档国家标准参见《电子文件存储与交换格式版式文档》。既然是国家标准,OFD随后肯定会首先在政务系统使用,并逐步推向社会各个方面。OFD是在研究当下各类文件格式后,推出的标准,有如下优点:
1 产权属于自主产权
2 具有便携性:文件小,可压缩比率大。测试显示生成的文件体量比PDF还要小。
3 具有开放性:易于入门,对于使用者来说更具开放性。
4 具有扩展性:预留了可扩展入口和自定义标引,设置了非接触式引用机制,为特性化提供支持。
5 呈现效果与设备无关,在各种设备上阅读、打印或印刷时,版面固定、不跑版。
6 应用广泛:无论是电子商务、电子公务,还是信息发布、文件交换,档案管理等都需要版式文档的技术支持。
关于标准,我也要吐槽一下。OFD标准是国内几家专业的电子文档处理公司参与起草的;标准文档(注:以下用”标准”特指OFD标准)只有126页,在我看来,标准对技术细节的描述过于简单,没有一定的技术背景很难看懂。与此形成鲜明对比的是pdf标准,有1000多页。我在网上也没找到文字版的标准,特别不利于阅读和参考。
我最近一直研究ofd标准,试图写一款阅读器,已初有成果。具有ofd转换为pdf、转为图片等特色功能。界面如下:
本文就把我开发的过程做简单介绍。
OFD标准简介
简而言之,OFD存储是采用压缩技术,描述采用XML格式。这一点与微软的word文档(docx)格式很类似。标准可能参考了微软的处理方式;在技术上也要实事求是,国标这种格式不是独创和领先的。将OFD格式文件解压后,会看到如下目录和文件:
文件中会包括资源文件(图片、字体库等)。XML会对资源存放,图元(文字、图像等)显示做描述,阅读软件会根据这些描述呈现出一致的显示效果。
开发OFD阅读软件步骤
国内流行的ofd阅读软件应该是福昕和数科开发的,这两款我都用过。我还要吐槽一下:
1)福昕阅读器帮助文档是ofd格式,但是无法用数科的阅读器打开。
2)有些ofd文档中xml标记,在标准中找不到,是某些公司独创的?
这些软件都是用C++开发的,用到了QT。同样情况下,相比于C#,C++开发软件难度肯定会大增。在windows平台开发界面,WPF应该是最好的库了。WPF虽然出现十几年了,大家好像对此还很陌生。主要现在是BS的天下;不是WPF不够好,是生不逢时。
1 对OFD文件解压缩
OFD文件其实就是压缩文件,解压后的文件也有目录结构。该模块的功能是获取每个文件的路径和数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; namespace WpfOfdReader.OfdFileType { class OfdFileReader { ZipArchive _zipArchive; public void ReadZipFile( string fileName) { _zipArchive = ZipFile.OpenRead(fileName); } public void Close() { if (_zipArchive != null ) _zipArchive.Dispose(); } public List<OfdFileItemInfo> AllFileItem { get { return _zipArchive.Entries.Select(o => new OfdFileItemInfo(o)).ToList(); } } private ZipArchiveEntry GetArchiveEntry(ZipFilePath path) { foreach (ZipArchiveEntry entry in _zipArchive.Entries) { if (entry.FullName == path.FulleName) { return entry; } } return null ; } public static byte [] GetFileBuffer(ZipArchiveEntry entry) { List< byte []> listBuffer = new List< byte []>(); using (Stream s = entry.Open()) { while ( true ) { byte [] buffer = new byte [10]; int n = s.Read(buffer, 0, buffer.Length); if (n <= 0) break ; if (n == buffer.Length) { listBuffer.Add(buffer); } else { Array.Resize( ref buffer, n); listBuffer.Add(buffer); break ; } } } int totalLen = 0; listBuffer.ForEach(o => totalLen += o.Length); byte [] result = new byte [totalLen]; int index = 0; foreach ( byte [] buffer in listBuffer) { Buffer.BlockCopy(buffer, 0, result, index, buffer.Length); index += buffer.Length; } return result; } } } |
2 找到需要展示的page
顺着路线 OFD.xml --> Document.xml --> Pages,找到最终需要展示的page页。Page页包含三类节点:PathObject、ImageObject,暨对应标准中的三类图元。需要对这三类节点建模。这三个类共同继承父类PageObject。所有的图元都有绘制区域、坐标变换、裁剪等共性,所有这些由PageObject类处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | public class PageObject { public string ID { get ; set ; } public PageLayer ParentLayer { get ; set ; } public string PageFileLoc => ParentLayer.ParentPage.PageFileLoc; XmlNode _xmlNode; public string Boundary { get ; set ; } public string CTM { get ; set ; } public OfdClipsGroup ClipsGroup { get ; set ; } public void SetPageObject(PageLayer layer, XmlNode xmlNode) { _xmlNode = xmlNode; ID = XmlHelper.GetXmlAttributeValue(xmlNode, "ID" ); ParentLayer = layer; Boundary = XmlHelper.GetXmlAttributeValue(xmlNode, "Boundary" ); CTM = XmlHelper.GetXmlAttributeValue(xmlNode, "CTM" ); foreach (XmlNode childNode in xmlNode.ChildNodes) { if (childNode.Name == OfdClipsGroup.XML_Name) { ClipsGroup = OfdClipsGroup.FromXml(childNode); break ; } } } public string GetAttributeValue( string name) { string result = XmlHelper.GetXmlAttributeValue(_xmlNode, name); return result; } } |
3 创建WPF显示模型
图像精确定位需要用到Canvas控件作为容器。绘制大量图形需要用到轻量级绘制模型DrawingVisual。在此基础上,派生了绘制基础模型OfdVisual,此模型对应PageObject。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | public class OfdVisual : DrawingVisual { public OfdVisual() { } protected DrawingCanvas _drawingCanvas; public DrawingCanvas DrawingCanvas { get { return _drawingCanvas; } } public bool IsAddToCanvas { get { return _drawingCanvas != null ; } } internal void AddToCanvas(DrawingCanvas drawingCanvas) { if (_drawingCanvas == drawingCanvas) return ; _drawingCanvas = drawingCanvas; _drawingCanvas.AddVisual( this ); } public void ReomveFromCanvas() { if (_drawingCanvas != null ) { _drawingCanvas.DeleteVisual( this ); } } public virtual void Show( bool visiable, bool even = false ) { } public Point BoundaryLocation { get ; set ; } public Size BoundarySize { get ; set ; } public MatrixTransform ObjectTransform { get ; protected set ; } public VisualClipsGroup ObjectClipsGroup { get ; protected set ; } public void SetPageObject(PageObject pageObject) { OfdHelper.ParseBoundary(pageObject.Boundary, out Point location, out Size size); BoundaryLocation = location; BoundarySize = size; if (! string .IsNullOrEmpty(pageObject.CTM)) { ObjectTransform = OfdHelper.OfdTextToTransform(pageObject.CTM); } if (pageObject.ClipsGroup != null ) { ObjectClipsGroup = new VisualClipsGroup() { ClipsGroup = pageObject.ClipsGroup }; } } protected Rect ClipRect { get { return new Rect(0, 0, BoundarySize.Width, BoundarySize.Height); } } protected RectangleGeometry ClipGeometry { get { RectangleGeometry geometry = new RectangleGeometry(ClipRect); return geometry; } } protected void PutBoundary(DrawingContext dc) { TranslateTransform translateBoundary = new TranslateTransform(BoundaryLocation.X, BoundaryLocation.Y); dc.PushTransform(translateBoundary); dc.PushClip(ClipGeometry); } protected void PopBoundary(DrawingContext dc) { dc.Pop(); dc.Pop(); } protected void PutTransform(DrawingContext dc) { if (ObjectTransform != null ) { dc.PushTransform(ObjectTransform); } } protected void PopTransform(DrawingContext dc) { if (ObjectTransform != null ) { dc.Pop(); } } } |
有三种类型绘制对象OfdVisualText、OfdVisualPath、OfdVisualImage,派生自OfdVisual。分别处理三种图元数据。所有的绘制操作在函数
1 | public override void Show( bool visiable, bool even = false ); |
对应文本,绘制函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | void DrawText() { using (DrawingContext dc = RenderOpen()) { if (ObjectClipsGroup == null ) { PutBoundary(dc); PutTransform(dc); DrawTextInner(dc); PopTransform(dc); PopBoundary(dc); } else { foreach (VisulClip visulClip in ObjectClipsGroup) { PutBoundary(dc); visulClip.PutClip(dc); PutTransform(dc); DrawTextInner(dc); PopTransform(dc); visulClip.PopClip(dc); PopBoundary(dc); } } } } private void DrawTextInner(DrawingContext dc) { int i = -1; double deltaXTotal = 0; double deltaYTotal = 0; Point pt = new Point(); foreach (FormattedText formattedText in FormattedTextCollection) { i++; if (i != 0) { if (DeltaCollectionX != null ) { double deltaX = DeltaCollectionX.GetValue(i - 1); deltaXTotal += deltaX; } if (DeltaCollectionY != null ) { double deltaY = DeltaCollectionY.GetValue(i - 1); deltaYTotal += deltaY; } } pt.X = TextLocation.X + deltaXTotal; pt.Y = TextLocation.Y + deltaYTotal - FormattedTextCollection.FontBaseLine; dc.DrawText(formattedText, pt); } } |
绘制前,需要对当前坐标做变换、旋转、剪切等操作。
最近又对程序完善了,增加缩略图和公文索引:
后记
编写阅读器类软件的关键是建模。首先读懂标准,对标准中描述的图元做归类分析,并建立起相应的显示模型。本人做WPF开发很多年了,感觉用WPF开发这类软件并不是非常的难。相比于QT,采用wpf开发有很多优势。如果要完整实现OFD标准,还需要大量的开发,我会逐步完善该软件的功能。
特别说明
ofd阅读器开发语言为c#,具有完全自主产权,没有使用第三方ofd开发包。可以根据你的需求快速定制开发。本阅读器还在开发完善阶段,如有任何问题,可以联系我QQ:13712486。
以上就是c# 基于wpf,开发OFD电子文档阅读器的详细内容,更多关于c# wpf开发的资料请关注自学编程网其它相关文章!
- 本文固定链接: https://zxbcw.cn/post/207096/
- 转载请注明:必须在正文中标注并保留原文链接
- QQ群: PHP高手阵营官方总群(344148542)
- QQ群: Yii2.0开发(304864863)