65.9K
CodeProject 正在变化。 阅读更多。
Home

XPS 简介 - 第 1 部分(共 n 部分,可能不止 n 部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (12投票s)

2008年4月19日

CPOL

15分钟阅读

viewsIcon

153425

downloadIcon

1159

XPS是一种源自XAML的固定文档格式。想知道如何使用它来生成您想要的文档吗?

目录

引言

XPS(XML Paper Specification)是一种固定页面格式规范,是PDF的一个有用替代品。正如PDF是PostScript的“精简”版本一样,XPS是XAML的一个精简模式版本,专门用于固定页面布局。由于XPS是基于XML的,它应该是生成自己文档的绝佳格式。不幸的是,似乎很少有资料能以一种对实际实现有用的方式来描述这种格式,当您想这样做的时候。我希望通过(简短的)一系列文章来填补其中的一些空白。

我对XPS的初步了解始于使用Word模拟排版文档,然后使用.NET v3提供的XPS打印机驱动程序将其“打印”出来,之后再检查XPS文档,以了解它们的结构以及如何进行操作。显然,如果您拥有MS Office 2007并获得了可选更新,您还可以选择“另存为”来生成XPS文档。

我发现Word和“XPS打印机”生成的XPS文件通常包含大量不必要的伪影(尤其是如果您编辑过文件多次,更改过字体等)。这个工具可以清理掉大量这些伪影,消除一些重复项,并进行一些其他调整,以帮助减小XPS文件的整体大小,尽管在大多数情况下,只减小几KB。逐步了解它的工作原理也有助于对XPS文件进行初步介绍。

如果您计划自己输出XPS,那么模拟您想要的格式并使用此工具来清理结果是一个非常方便的开始方式。

最初,我之所以研究XPS,纯粹是为了一个计费系统的概念验证。然而,当发现用于PDF/PostScript生成的商业系统价格“极其昂贵”时,“概念验证”就变成了实际的生产系统。

该项目的这部分源于将营销材料清理干净并准备好纳入客户账单的需要。这篇CodeProject文章就源于这项工作。

XPS内部结构

XPS文件使用的OOXML组织包含大量跨不同部分(文件)和单个文件内的交叉引用。我不会去争论OOXML是好是坏,关于这一点已经有很多争论了。然而,为了增加混乱,OOXML的“规范”已针对XPS进行了轻微调整。

就XPS文件而言,内部结构可以看作有三个层级(请注意,这不是官方解释,但对我来说很有效)。最顶层是XPS文件本身。接下来是其中包含的各个文档。最后是每个文档的各个页面。在这些层级中的每一个层级,都可能包含指向其他部分以及各种类型资源的引用。当然,这一切都是非常粗略的简化,但您应该明白了。

OOXML结构中的许多部分(文件)可以被赋予不同的名称,而不是“XPS打印机”使用的名称或样本文件中显示的名称,只要所有交叉引用都匹配即可。

在每个文件内部,各个部分通常不必按特定顺序排列。只有在页面布局方面,顺序才可能变得重要。否则,只需按照最方便处理的方式进行即可。

在您自己“打印”了一个示例XPS文件(并将其重命名为.zip文件)后,您可能会看到类似以下结构的目录

在XPS文档的每个层级,您可以找到三个不同的文件夹,尽管它们不必出现在每个层级

文件夹名称 描述
_rels 包含描述此层级的文件与XPS文件内其他部分的关系的文件。
元数据 存储与此层级相关的元数据文件。例如,文档的缩略图或PrintTicket文件。
资源 包含此XPS文件层级使用的资源(例如,字体和图像)。

根层级(XPS文件)

在根层级,会有两个文件

[Content_Types].xml 列出不同类型的文件,特别是此XPS文档中包含的文件扩展名。
FixedDocumentSequence.fdseq 将列出XPS文件中包含的实际文档,实际上指向层级中的下一层。

[Content_Types].xml通常看起来像这样

<types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<default contenttype="application/vnd.openxmlformats-package.relationships+xml"  
    extension="rels" />
<default contenttype="application/vnd.ms-package.xps-fixeddocumentsequence+xml" 
    extension="fdseq" />
<default contenttype="application/vnd.ms-package.xps-fixeddocument+xml" 
    extension="fdoc" />
<default contenttype="application/vnd.ms-printing.printticket+xml" 
    extension="xml" />
<default contenttype="image/jpeg" extension="JPG" />
<default contenttype="application/vnd.ms-package.xps-fixedpage+xml" 
    extension="fpage" />
<default contenttype="application/vnd.ms-package.obfuscated-opentype" 
    extension="odttf" />
</types>

请注意Root Types元素中的架构命名空间声明,以及“rels”扩展声明,这些都特定于OOXML。接下来是“fdseq”、“fdoc”和“fpage”扩展,它们都声明了XPS结构的各个部分。然后是“odttf”,用于混淆的开放类型字体文件;关于这些将在另一篇文章中讨论。不幸的是,“xml”被用作元数据PrintTicket文件的扩展名。最后是“JPG”和“PNG”,用于图像文件;您也可能看到其他文件,具体取决于您原始源文档中的内容。您可以假定“JPG”总是会存在,因为XPS打印机驱动程序生成的元数据缩略图始终是一个小的JPEG图像。

FixedDocumentSequence.fdseq通常非常简单。不仅是它的名称,它的扩展名也告诉我们它是一个固定文档序列文件。对于XPS打印机驱动程序生成的文档,它应该始终如下所示

<fixeddocumentsequence xmlns="http://schemas.microsoft.com/xps/2005/06">
    <documentreference source="/Documents/1/FixedDocument.fdoc" />
</fixeddocumentsequence>

现在我们知道去哪里找文档的第一部分了。但是,我们应该先看看_rels文件夹。

第一个文件称为.rels;实际上,这是与[Content_Types].xml文件对应的关系文件。它通常看起来像这样

<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Target="/FixedDocumentSequence.fdseq" Id="R0"
 Type="http://schemas.microsoft.com/xps/2005/06/fixedrepresentation"/>
<Relationship Target="/Documents/1/Metadata/Page1_Thumbnail.JPG" Id="R1"
 Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"/>
</Relationships>

您可以看到它标识了根层级的FixedDocumentSequence.fdseq文件,并为其分配了任意IDR0。它还标识了元数据缩略图,这将是整个XPS文件的缩略图。

另外,在_rels文件夹中还有FixedDocumentSequence.fdseq.rels - 它的用途应该相当明显

<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Target="/Metadata/Job_PT.xml" Id="R0"
    Type="http://schemas.microsoft.com/xps/2005/06/printticket"/>
</Relationships>

在这里,唯一描述的关系是指向元数据PrintTicket文件。PrintTickets也将在另一篇文章中进行描述。该文件通常是根层级Metadata文件夹中唯一的文件。

文档层级

在根目录下还将有Documents文件夹。此文件夹将包含XPS文件中的实际文档。当使用.NET v3 XPS打印机驱动程序时,此文档(在其自己的子文件夹中)始终命名为“1”,尽管文档实际上可以具有任何名称。通常,文档中使用的字体和图像等资源将在此层级下的Resources下。

在“1”文件夹下是上面FixedDocumentSequence.fdseq中引用的FixedDocument.fdoc。此文件列出了要显示或打印的页面的顺序。

页面层级

最后,每个文档子文件夹将包含一个Pages子文件夹,每个Pages子文件夹包含各个页面文件。此时还将有一个_rels文件夹,其中包含与每个.fpage文件对应的.rels文件。

如果您打开每个页面文件,您会清楚地看到XPS如何是XAML的一个受限子集,其中包含PathGlyphs元素。但是,如果您看到页面布局的不同部分似乎在文件中随意散布,也不要感到惊讶。只要没有z轴问题(即一个元素必须出现在另一个元素后面),XPS打印机驱动程序就会按照它认为合适的顺序输出页面上的各个元素。

<FixedPage Width="816" Height="1056" 
    xmlns="http://schemas.microsoft.com/xps/2005/06" xml:lang="und">
    <Glyphs Fill="#ff000000" 
        FontUri="/Documents/1/Resources/Fonts/87850AD7-9FD8-4CF2-9ED3-D635DE0AC70C.odttf" 
        FontRenderingEmSize="22.5173" StyleSimulations="None" 
        OriginX="105.6" OriginY="106.88" 
        Indices="44;81;87;85,42;82,52;71,55;88,55;70,45;87,34;76,27;82,51;81" 
        UnicodeString="Introduction" />
    <Path Data="F1 M 105.6,109.28 L 228.16,109.28 228.16,111.52 105.6,111.52 z" 
        Fill="#ff000000"/>
    <Glyphs Fill="#ff000000" 
        FontUri="/Documents/1/Resources/Fonts/87850AD7-9FD8-4CF2-9ED3-D635DE0AC70C.odttf" 
        FontRenderingEmSize="22.5173" StyleSimulations="None" 
        OriginX="228.16" OriginY="106.88" 
        Indices="3" UnicodeString=" " />
    <Glyphs Fill="#ff000000" 
        FontUri="/Documents/1/Resources/Fonts/BCA29EFB-F86B-4B42-A6B7-754D68DD5A3A.odttf" 
        FontRenderingEmSize="15.0115" StyleSimulations="None"  
        OriginX="105.6" OriginY="132.96"  
        Indices="59,71;51;54;3,34;11,34;59,71;48,91;47,57; ... ;82,48;3" 
        UnicodeString="XPS (XML Paper Specification) is a ... useful alternative to " />
    ...

FixedPage是所有页面的根元素。FixedPage元素可以包含许多其他元素,但XPS打印机驱动程序通常只留下Path(图形)和Glyphs(文本)元素。

在实际输出Glyphs时,优先使用Indices而不是UnicodeString。我有时会发现这导致了一些有趣的输出。Indices属性是所有要使用的字形的列表。如果存在,则每个Indices条目必须在UnicodeString中有一个对应的字符。索引列表中的每个条目包括一个字形ID,可选后跟一个逗号,然后是一个AdvanceWidth,最后以分号分隔。Indices中实际上可以包含更多内容,但这基本上是XPS打印机驱动程序输出的极限。如果您想要两端对齐、居中或右对齐的文本,那么Indices属性是必不可少的;移除它,您将只得到简单的左对齐文本,没有任何特殊技巧。尽管如此,有一种特殊的技巧可以输出右对齐文本,而无需深入研究字体文件,我将在另一篇文章中介绍。

在上面的摘录中,您可以看到一些可以被“清理”掉的冗余伪影。在Path元素的Data属性中,“M”和“L”后面的空格是不必要的,终止“z”前面的空格也是如此。具有“ ”的UnicodeStringGlyphs元素是完全不必要的,下一个Glyphs元素的UnicodeString(和Indices)属性末尾的空格也可以消除。这些可能看起来不多,但经过大量编辑的Word文档通常会产生大量相应的XPS文件中的此类伪影;移除这些,您通常可以同时移除一些嵌入的字体文件,从而大大减小文件大小。

其他冗余伪影可以通过比较XPS文件内的所有文件来识别,寻找重复项并保留找到的副本。之后,引用重复副本的文件可以将其引用更改为指向原始文件。

说到混淆的字体文件,这些实际上是完整字体文件中仅提取文档所需字符的部分。当您想以编程方式输出XPS(不使用.NET XPS方法)并发现某些字符神秘消失时,这可能会变得很有趣。

Using the Code

这是一个简单的控制台应用程序,旨在从命令行执行。将您要清理的XPS文件名作为参数传递给它。它将描述它正在进行的步骤,最后,您将得到一个文件名末尾附加“-clean”的输出文件。

请阅读本文底部的“其他内容”,因为您将需要获取ICSharpCode zip库才能使其正常工作,并且我没有将其DLL包含在Zip文件中。

将此简单应用程序转换为服务或DLL将非常简单。

这段代码实际上应该被视为一个XML管道,事实上,它的许多操作都可以改为将各个文档作为流从一个步骤传递到下一个步骤,而不是像我在这里使用中间文件。但是,我将其结构化为这样,以便您可以注释掉删除中间文件的代码,然后进入其中查看它们。

此外,在清理了大量不必要的伪影之后,构成文件“清理”版本的各个部分也更有意义。

工作原理

首先,应用程序加载了执行大部分实际工作的四个XSLT。

// Load up the Cleanup XSLT
XslCompiledTransform cleanupXSLT = new XslCompiledTransform();
cleanupXSLT.Load("Resources\\XPSCleaner.xsl");
Console.WriteLine("Cleanup XSLT Loaded.");

// Load up the Resource Relationships XSLT
XslCompiledTransform relsXSLT = new XslCompiledTransform();
relsXSLT.Load("Resources\\XPSRels.xsl");
Console.WriteLine("Resource Relationships XSLT Loaded.");

// Load up the Resource Relationship Listing XSLT
XslCompiledTransform relRefsXSLT = new XslCompiledTransform();
relRefsXSLT.Load("Resources\\XPSRelRefs.xsl");
Console.WriteLine("Resource Relationship Listing XSLT Loaded.");

// Load up the References XSLT
XslCompiledTransform referencesXSLT = new XslCompiledTransform();
referencesXSLT.Load("Resources\\XPSReferences.xsl");
Console.WriteLine("References XSLT Loaded.");

接下来,打开原始XPS文件,并将每个文件与同类型、同大小的每个其他文件进行比较,以识别重复项。在构建清理后的XPS时,这些重复项将被丢弃,并且其他文件中对它们的引用也将被修改。这段代码不那么优雅,但它能完成工作。

// Duplicate files will be dropped and references to them
// altered to point to the 'original' 
foreach (ZipEntry ze1 in zf)
{
    string ze1NewName = ze1.Name.Replace("Documents/1/", "Documents/2/");
    // Skip this file if we've already identified it as a duplicate
    if (dupFiles.ContainsKey(ze1NewName))
        continue;

    // Go back through the list to identify any duplicates
    foreach (ZipEntry ze2 in zf)
    {
        // Ready the stream for the 'original' file
        using (Stream zs1 = zf.GetInputStream(ze1))
        {
            string ze2NewName = ze2.Name.Replace("Documents/1/", 
                                                 "Documents/2/");

            // Skip this file if it happens to be the same one
            // or is not the same type (extension)
            // or are of differing file sizes
            if (ze1NewName == ze2NewName ||
                Path.GetExtension(ze1NewName) != Path.GetExtension(ze2NewName) ||
                ze1.Size != ze2.Size)
                continue;

            bool isEqual = true;

            // Ready some small buffers for the comparison
            byte[] buffer1 = new byte[4096];
            byte[] buffer2 = new byte[4096];
            int sourceBytes1;
            int sourceBytes2;

            // Now open up the two files and check if they are the same
            using (Stream zs2 = zf.GetInputStream(ze2))
            {
                // Using a fixed size buffer here makes no noticeable difference 
                // for performance but keeps a lid on memory usage.
                do
                {
                    sourceBytes1 = zs1.Read(buffer1, 0, buffer1.Length);
                    sourceBytes2 = zs2.Read(buffer2, 0, buffer2.Length);

                    for (int i = 0; i < buffer1.Length; i++)
                    {
                        if (buffer1[i] != buffer2[i])
                        {
                            isEqual = false;
                            break;
                        }
                    }

                    // If filesize can be relied on
                    // this test should never fire
                    if (sourceBytes1 != sourceBytes2)
                    {
                        isEqual = false;
                    }

                } while (sourceBytes1 > 0 && isEqual);
            } 

            if (isEqual)
            {
                // This file must be identified as a duplicate
                dupFiles.Add(ze2NewName, ze1NewName);
            }
        }
    }
}

然后,实际的清理阶段开始,XPS中的每个非资源或元数据文件都会被逐个处理,并在处理后放入输出XPS文件。一个常见的更改是将所有文件和引用从文档“1”移动到文档“2”。做这样的事情可以更容易地将由XPS打印机驱动程序生成的一个XPS文件与另一个合并。

页面文件(.fpage)通过清理XSLT进行处理,以删除冗余引用并进行一些其他调整;相应的.rels文件也从此“清理”后的页面文件中重新生成。然后,该文件被处理以构建实际使用的资源和元数据列表。

// Clean up the .fpage file itself
// Also alters references to 'duplicate' files 
string entryFileName = CopyAndCleanFile(baseFileName, processingFileName, 
                                        cleanupXSLT, zf, ze, s);

// Determine the temporary file names we'll use
string relsFileName = baseFileName + "Rels." + 
                      Path.GetFileName(processingFileName);
string processingRelsFileName = 
    processingFileName.Replace("Documents/1/Pages/", 
    "Documents/2/Pages/_rels/") + ".rels"

// Generate the Associated .rels file (removing any redundant references)
relsXSLT.Transform(entryFileName, relsFileName);
Console.WriteLine("{0} has been generated.", processingRelsFileName);

// Delete the cleaned file
File.Delete(entryFileName);

// Do a search and replace for each of the 'duplicate' file references
ReplaceReferencesToDuplicates(relsFileName, dupFiles);

// Add the generated rels entry to the new zip
AddZipEntry(processingRelsFileName, s, relsFileName);

// Identify the actual resources needed
relRefsList = IdentifyRels(relRefsList, relRefsXSLT, relsFileName);
Console.WriteLine("{0} Resources have been listed.", 
                  processingRelsFileName);

// Delete the rels file
File.Delete(relsFileName);

下面是执行此页面文件清理工作大部分工作的XSLT。NET 3中现有的XPS方法侧重于简单的XPS输出生成。要实际操作它,需要切换到类似XSLT的工具。

需要注意的是,这些XSLT是专门为适应至少可以追溯到MSXML 3的Microsoft XSLT的一个怪癖而设置的。在每个模板中,每个要创建的元素都必须声明正确的命名空间(除非它在另一个元素内创建),MS XSLT处理器在意识到它不需要时会将其丢弃。如果没有命名空间声明,MS XSLT处理器会在您的元素中插入一个空的命名空间声明(xmlns=""),这确实会造成很大的麻烦。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:x="http://schemas.microsoft.com/xps/2005/06"
    exclude-result-prefixes="x">

    <xsl:output indent="yes" method="xml" 
       encoding="utf-8" omit-xml-declaration="yes"/>

    <xsl:template match="/">
        <!-- Work our way through every element -->
        <xsl:apply-templates select="*"/>
    </xsl:template>

    <xsl:template match="x:Glyphs">
        <!-- Include Glyphs that aren't all whitespace -->
        <xsl:if test="string-length(normalize-space(@UnicodeString)) &gt; 0">
            <Glyphs xmlns="http://schemas.microsoft.com/xps/2005/06">
                <xsl:apply-templates select="@*"/>
            </Glyphs>
        </xsl:if>
    </xsl:template>

    <xsl:template match="*">
        <!--  General processing for all other elements -->
        <xsl:element name="{name(.)}" 
            namespace="http://schemas.microsoft.com/xps/2005/06">
            <xsl:apply-templates select="@*"/>
            <xsl:choose>
                <xsl:when test="count(*) &gt; 0">
                    <xsl:apply-templates select="*"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="."/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:element>
    </xsl:template>

    <xsl:template match="@Data[name(..) = 'Path']">
        <!-- Clean up the Data attribute of Path elements -->
        <xsl:attribute name="Data">
            <xsl:call-template name="CleanPath">
                <xsl:with-param name="pathData" select="."/>
            </xsl:call-template>
        </xsl:attribute>
    </xsl:template>

    <xsl:template match="@UnicodeString">
        <!-- Clean up the UnicodeString attribute -->
        <xsl:attribute name="UnicodeString">
            <xsl:call-template name="CleanUnicodeString">
                <xsl:with-param name="unicodeString" select="."/>
            </xsl:call-template>
        </xsl:attribute>
    </xsl:template>

    <xsl:template match="@Indices">
        <!-- Clean up the Indices attribute, 
        removing indices for redundant whitespace from the end -->
        <!-- The source string is first of all reversed,
        then redundant indices removed from what is now the 'front' of the string,
        then it's all reversed back again -->
        <xsl:attribute name="Indices">
            <xsl:call-template name="StringReverse">
                <xsl:with-param name="string">
                    <xsl:call-template name="CleanIndices">
                        <xsl:with-param name="indices">
                            <xsl:call-template name="StringReverse">
                                <xsl:with-param name="string" select="."/>
                            </xsl:call-template>
                        </xsl:with-param>
                    </xsl:call-template>
                </xsl:with-param>
            </xsl:call-template>
        </xsl:attribute>
    </xsl:template>

    <xsl:template match="@*">
        <!-- General processing for all other attributes -->
        <xsl:attribute name="{name(.)}">
            <xsl:choose>
                <xsl:when test="starts-with(., '/Documents/1/Resources/Fonts/')">
                    <!-- Move the fonts down to the XPS root -->
                    <xsl:value-of select="substring-after(., '/Documents/1')"/>
                </xsl:when>
                <xsl:when test="starts-with(., '/Documents/1')">
                    <!-- Move everything else to document number 2 - because we can -->
                    <xsl:value-of select="concat('/Documents/2', 
                        substring-after(., '/Documents/1'))"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="."/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:attribute>
    </xsl:template>

    <xsl:template name="CleanPath">
        <!-- Clean path data eliminating redundant whitespace -->
        <xsl:param name="pathData" select="''"/>

        <xsl:choose>
            <xsl:when test="contains($pathData, '  ')">
                <xsl:call-template name="CleanPath">
                    <xsl:with-param name="pathData"
                        select="concat(substring-before($pathData, '  '), ' ', 
                            substring-after($pathData, '  '))"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($pathData, ' M ')">
                <xsl:call-template name="CleanPath">
                    <xsl:with-param name="pathData"
                        select="concat(substring-before($pathData, ' M '), ' M', 
                            substring-after($pathData, ' M '))"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($pathData, ' L ')">
                <xsl:call-template name="CleanPath">
                    <xsl:with-param name="pathData"
                        select="concat(substring-before($pathData, ' L '), ' L', 
                            substring-after($pathData, ' L '))"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($pathData, ' z')">
                <xsl:call-template name="CleanPath">
                    <xsl:with-param name="pathData"
                        select="concat(substring-before($pathData, ' z'), 'z', 
                            substring-after($pathData, ' z'))"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$pathData"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template name="CleanUnicodeString">
        <!-- Clean unicode string removing redundant whitespace from the end -->
        <xsl:param name="unicodeString" select="''"/>

        <xsl:if test="substring($unicodeString, string-length($unicodeString), 1) = ' '">
            <xsl:value-of select="substring($unicodeString, 1, 
                string-length($unicodeString) - 1)"/>
        </xsl:if>
    </xsl:template>

    <xsl:template name="CleanIndices">
        <!-- Clean indices removing redundant whitespace references
        from the end (reversed to be at the beginning) -->
        <xsl:param name="indices" select="''"/>

        <xsl:choose>
            <xsl:when test="starts-with($indices, '3;')">
                <!-- Strip off simple spaces -->
                <xsl:call-template name="CleanIndices">
                    <xsl:with-param name="indices" 
                        select="substring-after($indices, '3;')"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:when test="contains($indices, ',') and
                        not(contains(substring-before($indices, ','), ';')) and
                        starts-with(substring-after($indices, ','), '3;') ">
                <!-- Strip off spaces with a size -->
                <xsl:call-template name="CleanIndices">
                    <xsl:with-param name="indices" 
                        select="substring-after($indices, '3;')"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$indices"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <xsl:template name="StringReverse">
        <!-- Take any given string and reverse it -->
        <xsl:param name="string"/>

        <xsl:variable name="len" select="string-length($string)"/>

        <xsl:choose>
            <xsl:when test="$len &lt; 2">
                <xsl:value-of select="$string"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:call-template name="StringReverse">
                    <xsl:with-param name="string" 
                        select="substring($string, $len div 2 + 1, $len div 2)"/>
                </xsl:call-template>
                <xsl:call-template name="StringReverse">
                    <xsl:with-param name="string" 
                        select="substring($string, 1, $len div 2)"/>
                </xsl:call-template>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

上述XSLT主要侧重于识别冗余的空格并将其消除。这偶尔会导致某种字体文件不再需要的情况,而这正是我们可以真正减小XPS文件大小的情况。

我本可以调用XSL的documents()函数来包含重复文件列表(格式为XML)并在处理中使用它。但是,这需要对预编译的XSLT如何生成进行进一步更改,因为这是一个潜在的安全风险,并且还需要对XSLT本身进行重大更改,以便它能够识别对“重复项”的引用并将其替换为对“原始项”的引用。我选择了一个更简单的解决方案,从编码角度来看,就是对上述XSLT的输出进行逐行搜索和替换。

接下来运行的XSLT从“清理”后的fpage文件中重新生成.rels文件,实际上是丢弃了对现已冗余的资源和/或元数据的引用。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:x="http://schemas.microsoft.com/xps/2005/06"
    exclude-result-prefixes="x">

    <xsl:output indent="yes" method="xml" 
       encoding="utf-8" omit-xml-declaration="yes"/>

    <xsl:key name="resourceKey" match="//@*[starts-with(., '/Resources/Fonts/') 
        or starts-with(., '/Documents/2/Resources/Images/') 
        or starts-with(., '/Documents/2/Metadata/')]" use="."/>

    <xsl:template match="/">
        <Relationships 
            xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
            <!-- Work our way through every unique resource attribute 
                using the Muenchian method -->
            <xsl:apply-templates 
                select="//@*[contains(., '/Resources/') or contains(., '/Metadata/')]
                    [generate-id() = generate-id(key('resourceKey', .))]"/>
            <!-- Add in a reference for the printticket 
                as this won't be found in the source page files -->
            <Relationship Type="http://schemas.microsoft.com/xps/2005/06/printticket" 
                Target="/Documents/2/Metadata/Page1_PT.xml">
                <xsl:attribute name="Id">
                    <xsl:value-of 
                        select="concat('R', count(//@*[starts-with(., '/Resources/Fonts/') 
                            or starts-with(., '/Documents/2/Resources/Images/') 
                            or starts-with(., '/Documents/2/Metadata/')]))"/>
                </xsl:attribute>
            </Relationship>
        </Relationships>
    </xsl:template>

    <xsl:template match="@*">
        <!-- List out the resource identifier -->
        <Relationship Type="http://schemas.microsoft.com/xps/2005/06/required-resource" 
            xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
            <xsl:attribute name="Target">
                <xsl:value-of select="."/>
            </xsl:attribute>
            <xsl:attribute name="Id">
                <xsl:value-of select="concat('R', position())"/>
            </xsl:attribute>
        </Relationship>
    </xsl:template>
</xsl:stylesheet>

好吧,如果Muenchian方法没有出现,这还不算是一个涉及XSLT的真正项目,对吧?这个XSLT确保我们每个唯一资源只有一个Relationship元素。

另一个XSLT处理所有需要调整资源引用的其他文件,因为我们将所有内容从“1”移动到“2”。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:x="http://schemas.microsoft.com/xps/2005/06" 
    xmlns:r="http://schemas.openxmlformats.org/package/2006/relationships" 
    exclude-result-prefixes="x r">

    <xsl:output indent="yes" method="xml" 
       encoding="utf-8" omit-xml-declaration="yes"/>

    <xsl:template match="/">
        <xsl:apply-templates select="*"/>
    </xsl:template>

    <xsl:template match="r:Relationships">
        <!-- Processing for the 'primary' elements of page related .rels files -->
        <!-- Actually this particular template should never be invoked,
            it's here as an 'insurance' policy against 'maintenance' -->
        <Relationships 
        xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="*[not(contains(@Target, '/Fonts/'))]"/>
        </Relationships>
    </xsl:template>

    <xsl:template match="r:Relationship">
        <!-- Processing for the 'primary' elements of other .rels files -->
        <Relationship 
        xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
            <xsl:apply-templates select="@*"/>
        </Relationship>
    </xsl:template>

    <xsl:template match="x:FixedDocument|x:FixedPage|x:FixedDocumentSequence">
        <!-- Processing for the 'primary' elements for other than .rels files -->
        <xsl:element name="{name(.)}" 
            namespace="http://schemas.microsoft.com/xps/2005/06">
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="*"/>
        </xsl:element>
    </xsl:template>

    <xsl:template match="*">
        <!-- Processing for all other elements -->
        <xsl:element name="{name(.)}" 
            namespace="http://schemas.microsoft.com/xps/2005/06">
            <xsl:apply-templates select="@*"/>
            <xsl:choose>
                <xsl:when test="count(*) &gt; 0">
                    <!-- If there are sub-elements process these -->
                    <xsl:apply-templates select="*"/>
                </xsl:when>
                <xsl:otherwise>
                    <!-- If there are no sub-elements 
                    then just take the contents of this element -->
                    <xsl:value-of select="."/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:element>
    </xsl:template>

    <xsl:template match="@*">
        <!-- Processing for all attributes -->
        <xsl:attribute name="{name(.)}">
            <xsl:choose>
                <xsl:when test="starts-with(., '/Documents/1/Resources/Fonts/')">
                    <!-- Alter font references to point to the 'root' resources folder -->
                    <xsl:value-of select="substring-after(., '/Documents/1')"/>
                </xsl:when>
                <xsl:when test="starts-with(., '/Documents/1')">
                    <!-- Alter all other document references to point to document '2' -->
                    <xsl:value-of select="concat('/Documents/2', 
                        substring-after(., '/Documents/1'))"/>
                </xsl:when>
                <xsl:otherwise>
                    <!-- Leave all other references alone -->
                    <xsl:value-of select="."/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:attribute>
    </xsl:template>
</xsl:stylesheet>

最后一个XSLT实际上产生了文本输出。这个XSLT设计用于读取所有.rels文件(每个页面的文件,以及“根”_rels文件夹中的另一个文件,以及任何其他文件),并简单地生成一个列表,我们可以对其进行处理以确定我们真正需要的资源和元数据文件。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:r="http://schemas.openxmlformats.org/package/2006/relationships"
    exclude-result-prefixes="r">

    <xsl:output indent="yes" method="text" 
               encoding="utf-8" omit-xml-declaration="yes"/>

    <xsl:template match="/">
        <!-- List all the relationship 'targets' the resources and metadata files -->
        <xsl:for-each select="r:Relationships/r:Relationship/@Target">
            <xsl:value-of select="."/>
            <xsl:value-of select="' '"/>
        </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

最后一个XSLT的输出是我们唯一不输出到临时文件的。它而是通过流推送到一个StringBuilder中,该StringBuilder稍后被处理成一个列表。

这便引出了处理原始XPS文件的第三阶段。在第三次传递中,我们在第二阶段处理的文件将被跳过(它们的处理输出已在新XPS文件中);相反,它会拾取所有资源和元数据文件,并使用上述列表将它们放入新XPS文件的正确位置。任何“重复”文件都会被丢弃(忽略),最后,其他所有未完成的文件也会在此时候被获取。

// If this is a 'duplicate' then we just dump it
// As you can see this code fragment is from inside a loop
if (dupFiles.ContainsKey(processingFileName))
    continue;

if ((processingFileName.StartsWith("Documents/1/Pages") 
    && processingFileName.EndsWith(".fpage")) ||
    (processingFileName.EndsWith(".fpage.rels")))
{
    // Skip these - we've already processed them
}
else if (processingFileName.StartsWith("Documents/1/Resources/Fonts"))
{
    #region Resource files that require 'moving' to the 'root' Resources folder
    string newFileName = processingFileName.Replace("Documents/1/", "");
    if (relsFileNames.Contains(newFileName))
    {
        Console.WriteLine("XPS file entry '{0}' moving to {1}", 
            processingFileName, 
            processingFileName.Replace("Documents/1/", ""));
        CopyZipEntry(ze.Name.Replace("Documents/1/", ""), s, zf, ze);
    }
    #endregion
}
else if (processingFileName.StartsWith("Documents/1/") || 
    processingFileName.Contains("_rels/") || 
    processingFileName.EndsWith(".fdseq"))
{
    // Identify the files that were cleaned up
    bool bTransformRequired = (processingFileName.EndsWith(".rels") || 
        processingFileName.EndsWith(".fdoc") || 
        processingFileName.EndsWith(".fdseq"));

    if (!bTransformRequired)
    {
        #region Files that only require 'moving' to document '2'
        string newFileName = 
          processingFileName.Replace("Documents/1/", "Documents/2/");
        if (relsFileNames.Contains(newFileName))
        {
            Console.WriteLine("XPS file entry '{0}' moving to {1}", 
            processingFileName, newFileName);
            CopyZipEntry(ze.Name.Replace("Documents/1/", 
                         "Documents/2/"), s, zf, ze);
        }
        #endregion
    }
}
else
{
    #region Files we just put in the same place in the new zip
    Console.WriteLine("XPS file entry '{0}' transferred as is", processingFileName);
    CopyZipEntry(ze.Name, s, zf, ze);
    #endregion
}

在将所有文件移到新位置(并静默删除冗余/重复的文件)后,新的XPS文件将被关闭,程序也就完成了对原始XPS的“清理”。

其他

此应用程序使用ICSharpCode SharpZipLib库进行zip文件的打包和解包(http://www.icsharpcode.net/OpenSource/SharpZipLib/Default.aspx)。它不包含在项目中,因此您需要单独下载。

我还使用了Stylus Studio进行XSLT编码(http://www.stylusstudio.com)。尽管在此特定应用程序中,XSLT的性质非常简单。

我还强烈建议阅读微软的官方XPS规范(http://www.microsoft.com/whdc/xps/downloads.mspx),并获取我从中获得许多见解的示例XPS文档(http://www.microsoft.com/whdc/XPS/XpsSamples.mspx)。另外,还可以查阅官方团队博客(http://blogs.msdn.com/xps/default.aspx)和Feng Yuan的博客(http://blogs.msdn.com/fyuan/default.aspx)。

我还推荐

我将尽量不重复所有这些人的工作。

除此之外,我还建议获取Windows驱动程序工具包(WDK)中的IsXPS.exe测试工具。

其他部分

历史

  • 2008-04-19: 完成第一个版本。
  • 2008-04-21: 添加了一些推荐阅读内容。
  • 2008-04-28: 添加了额外的处理阶段,用于识别重复文件并将其消除。
© . All rights reserved.