如何手動或程式化產生XPS文件

文:吳俊毅
時間:2009/1/18

何謂XPS文件
XPS為(XML Parse Specification)的簡稱,我們知道XML已經是開放式資料交換的標準,於是以XML為基礎要提供一個更穩固的,開放的可信賴的電子文件交換格式應運而生,就是XPS,它可以經由硬體或是軟體來閱讀並提供更佳的列印品質,並可加入簽章容易用於機密文件的封存,微軟在Office 2007也已經完整加入此功能,Word 2007的docx其實就是XML Parse Specification文件格式。在.NET Framework 3.0中提供完整的API讓您能輕易的發佈或是匯入XPS文件,或是與WPF應用程式的整合等,今天筆者將完整介紹XPS,產生XPS方式,如何使用.NET Framework 3.o的API來產生XPS格式文件,廢話不多說,我們開始吧!

XPS文件的結構
所謂的XPS文件其實是一個ZIP檔案,若讀者將其附檔名改成.zip使用Winzip或是一些支援ZIP的檢視器即可看見期內容,當然Windows XP/Vista的檔案總管本身即也支援ZIP解壓縮。

雖然我們通常並不會直接去編輯XPS文件的壓縮內容,但了解其內部包裝的內容對於我們快速的了解XPS是有幫助的,一個XPS文件的內容如下:

XPS的物件階層
XPS文件定義在.NET Framework 3.0定義在XpsDocument類別中,該類別則定義在System.Windows.Xps.Package;的命名空間中,XpsDocument為一個完整XPS檔案組成,它定義了XPS整個的邏輯結構,由於一個XpsDocument是允許包含多個文件,這些文件則由FixedDocumentSequence來描述其順序關係,所以通常一個XpsDocument下會包含一個FixedDocumentSequence物件,FixedDocumentSequence下面會有FixedDocument物件來包裝FixedPage物件,整個XpsDocument結構如下圖:
產生XPS文件的幾種方式
1. 使用Microsoft XPS Document Writer印表機
2. 使用.NET Framrwork 3.0的System.Windows.Xps.Package命名空間提供的類別來產生

要產生XPS文件最快的方式當然是使用Microsoft XPS Document Writer印表機了,當您安裝.NET Framework 3.0時就會自動被安裝進來,如下:產生XPS文件的幾種方式
1. 使用Microsoft XPS Document Writer印表機
2. 使用.NET Framrwork 3.0的System.Windows.Xps.Package命名空間提供的類別來產生

要產生XPS文件最快的方式當然是使用Microsoft XPS Document Writer印表機了,當您安裝.NET Framework 3.0時就會自動被安裝進來,如下:


產生XPS文件的幾種方式
1. 使用Microsoft XPS Document Writer印表機
2. 使用.NET Framrwork 3.0的System.Windows.Xps.Package命名空間提供的類別來產生

要產生XPS文件最快的方式當然是使用Microsoft XPS Document Writer印表機了,當您安裝.NET Framework 3.0時就會自動被安裝進來,如下:

各位若要快速產生XPS文件只要在您的文件列印時選擇該印表機即可,但是當有需求需要使用C#程式碼動態來產生XPS文件內容時該怎麼做呢,筆者來做個範例,這個範例會產生一個XpsDocument的頁面並對該頁面插入一個Hello World!字串,然後在下面再插入一張圖片。

首先,還是先開一個WPF專案,這個專案必須加入ReachFramework.DLL這個Assembly,因為System.Window.Xps命名空間所有類別均定義在此。

如圖,加入一個ReachFramework的參考:

這個專案筆者命名為WpfXpsCreate2,筆者將Wpf的Grid配置如下圖,透過一個Button來產生一個內含一個圖片的XPS文件:

而要實做這樣的功能我們必須先產生一個XpsDocument物件,使用這個XpsDocument產生一個XpsFixedDocumentSequence物件以便定義文件的序列,一個Xps文件最少一定有一個序列,才可定義內部所有與之相關的頁面的順序,這個文件的序列要使用一個IXpsFixedDocumentSequenceWriter寫入器來進行寫入的動作,有了XpsFixedDocuemntSequenceWriter物件之後使用它來產生Page,範例程式如下:
XpsDocument xpsDoc = new XpsDocument(“你要產生的.xps”, FileAccess.ReadWrite);
IXpsFixedDocumentSequenceWriter fds = xpsDoc.AddFixedDocumentSequence();
IXpsFixedDocumentWriter fd = fds.AddFixedDocument();
IXpsFixedPageWriter fp = fd.AddFixedPage();

對Xps文件而言,不管是字型檔或是圖片仍然是一種Resource,圖片是使用XpsResource物件加以定義,也就是說必須先定義一個XpsResource物件,然後呼叫XpsFixedDocumentWriter的AddImage方法:
XpsResource res = fp.AddImage(XpsImageType.JpegImageType);

這個圖片要在XpsDocument中顯示的話必須加入封裝,這個封裝就是Xps的Package,前面提到它以Zip為封裝的格式,所以在AddImage之後要設定一個封裝,設定方式如下:
XpsResource thumb = xpsDoc.AddThumbnail(XpsImageType.JpegImageType);

接著必須設定字型檔,因為我們要在XPS文件中顯示”Hello World”的字串,我們可以使用XpsFixedPageWriter的AddFont方法將字型加入到XpsResource中,但有些字型檔案過於龐大,而在XPS文件中支援一種Embedded Font Obfuscated字型,詳細的定義在XPS規格定義中,有興趣可以參考如下網址:(http://www.microsoft.com/taiwan/whdc/xps/xpsspec.mspx),這種字型英文翻作Obfuscated TTF,附檔名為odttf,而它的出現是為了解決XPS & Silverlight等字型授權的問題,避免有人從盜用XPS文件或是Silverlight內的內嵌字型,因此Obfuscated TTF可以將TTF字型萃取出畫面有使用到的字型,所以就算你把odttf字型從XPS的封裝中解壓縮出來也是無法直接使用的,為了產生Obfuscated TTF微軟提供了一個WriteObfuscatedStream方法,由於它的詳細實做不在本文討論的範圍所以筆者就不詳細說明了,加入Font資源筆者用下面三行程式碼解決:
XpsResource xpsFont = fp.AddFont();
WriteObfuscatedStream(xpsFont.Uri.ToString(), xpsFont.GetStream(), "arial.ttf");
xpsFont.Commit();

最後一行的Commit動作一定要執行才會真的更新到XpsResource中,剛剛加入的圖片也必須產生Stream並將資料流寫入並執行Commit動作,程式如下:
WriteStream(res.GetStream(), resource_File);
WritePageContent(fp.XmlWriter, res, xpsFont);
res.Commit();

WriteStream(thumb.GetStream(), resource_File);
thumb.Commit();

上面程式碼中讀者一定注意到有一個WritePageContent方法,這個方法就是寫入這個XPS文件的XML內容,不,更正確的說應是XAML才對,因為XPS文件與WPF與Silverlight關係密切,為什麼筆者這麼說呢,WPF與Silverlight絢麗的圖形UI是向量圖型方式繪製,若讀者開發過WPF應用程式一定知道畫布(Cavans)物件,在WPF與Silverlight透過它可以以絕對位置將控制項或圖片繪製在Cavans的任何一個地方,而XPS文件顯示區塊其實就是一個畫布,意思是說XPS文件內容是用WPF的畫布畫出來的,它為WPF當中的一個子集,因此這也是為什麼微軟說XPS文件透過雷射印表機可以印出更複雜與絕佳的色彩效果,所以OK,回到正題,WritePageContent就是筆者產生XAML語法的一個方法,在這裏我們必須先思考這個畫布上會有什麼?會有一個Hello Word與一張圖片,因此筆者先設計好這個XAML程式碼,內容如下:

XAML程式碼中筆者使用了Glyphs物件,在WPF中文字物件顯示方還有TextBlock與Path向量座標描述方式,Path為WPF中佔有非常重要的地位,有機會筆者再向大家介紹,使用Glyphs必須指定相對應的字型檔案TTF或是ODTTF檔案,需注意的是FontUri與Path的ImageSource屬性筆者先使用絕對路徑,因為為了在Kaxaml中可以直接預覽我們這個XPS的文件的呈現效果如何,到時這兩個屬性在Runtime時會寫入的是XPS封裝中的相對路徑,而筆者現在只要將這段XAML貼在Kaxaml中即可預視我們這個XPS文件的內容,如下圖:

如上圖,一個Hello World字串與一張圖形這就是到時我們產生的XPS文件的實際內容,接下來筆者就要開始撰寫WritePageContent方法,這個方法要使用XmlWriter物件寫入這個XAML內容,筆者傳入XmlWriter、XpsRes、XpsFont三個物件,後兩個為XpsResource,程式碼如下:
private void WritePageContent(
System.Xml.XmlWriter xmlWriter,
XpsResource xpsRes,
XpsResource xpsFont)
{
xmlWriter.WriteStartElement("FixedPage");
xmlWriter.WriteAttributeString("Width", "816");
xmlWriter.WriteAttributeString("Height", "1056");
xmlWriter.WriteAttributeString("xmlns", @"http://schemas.microsoft.com/xps/2005/06");
xmlWriter.WriteAttributeString("xml:lang", "en-US");
xmlWriter.WriteStartElement("Canvas");

xmlWriter.WriteStartElement("Glyphs");
xmlWriter.WriteAttributeString("Fill", "#ff000000");
xmlWriter.WriteAttributeString("FontUri", xpsFont.Uri.ToString());
xmlWriter.WriteAttributeString("FontRenderingEmSize", "18");
xmlWriter.WriteAttributeString("OriginX", "20");
xmlWriter.WriteAttributeString("OriginY", "20");
xmlWriter.WriteAttributeString("UnicodeString", "Hello World");
xmlWriter.WriteEndElement();

if (xpsRes is XpsImage)
{
xmlWriter.WriteStartElement("Path");
xmlWriter.WriteAttributeString("Data", "M 20,20 L 770,20 770,770 20,770 z");
//xmlWriter.WriteAttributeString("Data", "M 120,187 L 301,187 301,321 120,321 z");
xmlWriter.WriteStartElement("Path.Fill");
xmlWriter.WriteStartElement("ImageBrush");
xmlWriter.WriteAttributeString("ImageSource", xpsRes.Uri.ToString());
xmlWriter.WriteAttributeString("Viewbox", "0,0,750,750");
xmlWriter.WriteAttributeString("ViewboxUnits", "Absolute");
xmlWriter.WriteAttributeString("Viewport", "20,30,750,750");
xmlWriter.WriteAttributeString("ViewportUnits", "Absolute");
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
}
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
}
如上程式筆者就不再說明WriteStartElement與WriteAttributeString方法的作用了,對了,剛剛的XAML原始碼中有些讀者對於Path物件下的Data屬性裡一長串的數字一定感到好奇,因為Path使用幾何路徑方式來進行繪圖,微軟又稱做迷你語言(Mini Language),功能非常強大,這真要另闢章節來討論了。

我們先來看看程式執行結果如何,產生的XPS文件是不是如Kaxaml預覽的那樣,程式執行畫面如下:

程式執行結束後會在Debug路徑下產生一個test.xps文件,我們立即打開test.xps看看結果。


果然,內容是不是與我們在Kaxaml預覽的相同呢,看到這裡相信讀者對於XPS文件有更深入的了解了,我想XPS的出現必定使PDF感受到壓力,它還有許多PDF所沒有的功能,在Windows Vista中XPS文件還能夠有漸層,透明度等效果,因Windows Vista的列印緩衝區就是以XPS來描述的,也就是說Vista使用XPS來改善列印的子系統。

註:Kaxaml是由Robby Ingebretsen建立用於代替XAMLPad的工具,可在flash2u神魂顛倒Silverlight- 點部落下載,網址如下:
http://www.dotblogs.com.tw/flash2u/archive/2008/08/04/4763.aspx


Q&A
問題(一):如果我的系統只有XAMLPad,那我可以使用XAMLPad來看效果嗎?

答:其實是可以的,但是必須將Cavans改放在Page物件下,不可以將Cavans放置在根,否則XAMLPad會看不懂。

問題(二):要檢視XPS文件一定要安裝.NET Framework 3.0嗎?
答:其實不盡然,.NET Framework 3.0還包括了XPS文件完整的類別的API定義與實作的部分,若你不是要跑上面的範例程式,微軟有出一個XPS Viewer EP的檢視器,EP是(Essentials Pack)的簡稱,提供給不希望安裝.NET Framework 3.0的Windows XP/2003安裝使用,可在下列網址下載。
http://www.microsoft.com/downloads/details.aspx?displaylang=zh-tw&FamilyID=b8dcffdd-e3a5-44cc-8021-7649fd37ffee

XPS Viewer EP檢視XPS文件的畫面如下:

注意:安裝XPS Viewer EP需要安裝Microsoft Core XML Service 6.0 以上才可順利執行。

下次筆者介紹如何對XPS加入簽章,下次見囉~

留言

這個網誌中的熱門文章

軟體工程師 - 成長的 10 個階段

常見的程式碼壞味道(Code Smell or Bad Smell)

什麼是 gRPC ?