1. 程式人生 > >採用WPF技術,開發OFD電子文件閱讀器

採用WPF技術,開發OFD電子文件閱讀器

前言 OFD是國家標準版式文件格式,於2016年生效。OFD文件國家標準參見《電子檔案儲存與交換格式版式文件》。既然是國家標準,OFD隨後肯定會首先在政務系統使用,並逐步推向社會各個方面。OFD是在研究當下各類檔案格式後,推出的標準,有如下優點:

1 產權屬於自主產權

2 具有便攜性:檔案小,可壓縮比率大。測試顯示生成的檔案體量比PDF還要小。

3 具有開放性:易於入門,對於使用者來說更具開放性。

4 具有擴充套件性:預留了可擴充套件入口和自定義標引,設定了非接觸式引用機制,為特性化提供支援。

5 呈現效果與裝置無關,在各種裝置上閱讀、列印或印刷時,版面固定、不跑版。

6 應用廣泛:無論是電子商務、電子公務,還是資訊釋出、檔案交換,檔案管理等都需要版式文件的技術支援。

  關於標準,我也要吐槽一下。OFD標準是國內幾家專業的電子文件處理公司參與起草的;標準文件(注:以下用”標準”特指OFD標準)只有126頁,在我看來,標準對技術細節的描述過於簡單,沒有一定的技術背景很難看懂。與此形成鮮明對比的是pdf標準,有1000多頁。我在網上也沒找到文字版的標準,特別不利於閱讀和參考。

  我最近一直研究ofd標準,試圖寫一款閱讀器,已初有成果,介面如下:

閱讀器下載地址 https://download.csdn.net/download/qq_29939347/11799156

 

 本文就把我開發的過程做簡單介紹。

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檔案其實就是壓縮檔案,解壓後的檔案也有目錄結構。該模組的功能是獲取每個檔案的路徑和資料。

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頁包含三類節點:TextObject、PathObject、ImageObject,暨對應標準中的三類圖元。需要對這三類節點建模。這三個類共同繼承父類PageObject。所有的圖元都有繪製區域、座標變換、裁剪等共性,所有這些由PageObject類處理。

    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。 

    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。分別處理三種圖元資料。所有的繪製操作在函式

public override void Show(bool visiable, bool even = false);

對應文字,繪製函式如下:

 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開發這類軟體並不是非常的難。我用不到兩週的時間,初步完成了OFD顯示開發。如果要完整實現OFD標準,還需要大量的開發,我會逐步完善該軟體的功