Xslt for word xml

Время на прочтение
16 мин

Количество просмотров 54K

Задача обработки документов в формате docx, а также таблиц xlsx и презентаций pptx является весьма нетривиальной. В этой статье расскажу как научиться парсить, создавать и обрабатывать такие документы используя только XSLT и ZIP архиватор.

Зачем?

docx — самый популярный формат документов, поэтому задача отдавать информацию пользователю в этом формате всегда может возникнуть. Один из вариантов решения этой проблемы — использование готовой библиотеки, может не подходить по ряду причин:

  • библиотеки может просто не существовать
  • в проекте не нужен ещё один чёрный ящик
  • ограничения библиотеки по платформам и т.п.
  • проблемы с лицензированием
  • скорость работы

Поэтому в этой статье будем использовать только самые базовые инструменты для работы с docx документом.

Структура docx

Для начала разоберёмся с тем, что собой представляет docx документ. docx это zip архив который физически содержит 2 типа файлов:

  • xml файлы с расширениями xml и rels
  • медиа файлы (изображения и т.п.)

А логически — 3 вида элементов:

  • Типы (Content Types) — список типов медиа файлов (например png) встречающихся в документе и типов частей документов (например документ, верхний колонтитул).
  • Части (Parts) — отдельные части документа, для нашего документа это document.xml, сюда входят как xml документы так и медиа файлы.
  • Связи (Relationships) идентифицируют части документа для ссылок (например связь между разделом документа и колонтитулом), а также тут определены внешние части (например гиперссылки).

Они подробно описаны в стандарте ECMA-376: Office Open XML File Formats, основная часть которого — PDF документ на 5000 страниц, и ещё 2000 страниц бонусного контента.

Минимальный docx

Простейший docx после распаковки выглядит следующим образом

image

Давайте посмотрим из чего он состоит.

[Content_Types].xml

Находится в корне документа и перечисляет MIME типы содержимого документа:

<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
    <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
    <Default Extension="xml" ContentType="application/xml"/>
    <Override PartName="/word/document.xml"
              ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>

_rels/.rels

Главный список связей документа. В данном случае определена всего одна связь — сопоставление с идентификатором rId1 и файлом word/document.xml — основным телом документа.

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship 
        Id="rId1" 
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
        Target="word/document.xml"/>
</Relationships>

word/document.xml

Основное содержимое документа.

word/document.xml

<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:o="urn:schemas-microsoft-com:office:office"
            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
            xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
            xmlns:v="urn:schemas-microsoft-com:vml"
            xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
            xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
            xmlns:w10="urn:schemas-microsoft-com:office:word"
            xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
            xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
            xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
            xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
            xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
            xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
            mc:Ignorable="w14 wp14">
    <w:body>
        <w:p w:rsidR="005F670F" w:rsidRDefault="005F79F5">
            <w:r>
                <w:t>Test</w:t>
            </w:r>
            <w:bookmarkStart w:id="0" w:name="_GoBack"/>
            <w:bookmarkEnd w:id="0"/>
        </w:p>
        <w:sectPr w:rsidR="005F670F">
            <w:pgSz w:w="12240" w:h="15840"/>
            <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" 
                     w:header="720" w:footer="720" w:gutter="0"/>
            <w:cols w:space="720"/>
            <w:docGrid w:linePitch="360"/>
        </w:sectPr>
    </w:body>
</w:document>

Здесь:

  • <w:document> — сам документ
  • <w:body> — тело документа
  • <w:p> — параграф
  • <w:r> — run (фрагмент) текста
  • <w:t> — сам текст
  • <w:sectPr> — описание страницы

Если открыть этот документ в текстовом редакторе, то увидим документ из одного слова Test.

word/_rels/document.xml.rels

Здесь содержится список связей части word/document.xml. Название файла связей создаётся из названия части документа к которой он относится и добавления к нему расширения rels. Папка с файлом связей называется _rels и находится на том же уровне, что и часть к которой он относится. Так как связей в word/document.xml никаких нет то и в файле пусто:

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>

Даже если связей нет, этот файл должен существовать.

docx и Microsoft Word

docx созданный с помощью Microsoft Word, да в принципе и с помощью любого другого редактора имеет несколько дополнительных файлов.

image

Вот что в них содержится:

  • docProps/core.xml — основные метаданные документа согласно Open Packaging Conventions и Dublin Core [1], [2].
  • docProps/app.xml — общая информация о документе: количество страниц, слов, символов, название приложения в котором был создан документ и т.п.
  • word/settings.xml — настройки относящиеся к текущему документу.
  • word/styles.xml — стили применимые к документу. Отделяют данные от представления.
  • word/webSettings.xml — настройки отображения HTML частей документа и настройки того, как конвертировать документ в HTML.
  • word/fontTable.xml — список шрифтов используемых в документе.
  • word/theme1.xml — тема (состоит из цветовой схемы, шрифтов и форматирования).

В сложных документах частей может быть гораздо больше.

Реверс-инжиниринг docx

Итак, первоначальная задача — узнать как какой-либо фрагмент документа хранится в xml, чтобы потом создавать (или парсить) подобные документы самостоятельно. Для этого нам понадобятся:

  • Архиватор zip
  • Библиотека для форматирования XML (Word выдаёт XML без отступов, одной строкой)
  • Средство для просмотра diff между файлами, я буду использовать git и TortoiseGit

Инструменты

  • Под Windows: zip, unzip, libxml2, git, TortoiseGit
  • Под Linux: apt-get install zip unzip libxml2 libxml2-utils git

Также понадобятся скрипты для автоматического (раз)архивирования и форматирования XML.
Использование под Windows:

  • unpack file dir — распаковывает документ file в папку dir и форматирует xml
  • pack dir file — запаковывает папку dir в документ file

Использование под Linux аналогично, только ./unpack.sh вместо unpack, а pack становится ./pack.sh.

Использование

Поиск изменений происходит следующим образом:

  1. Создаём пустой docx файл в редакторе.
  2. Распаковываем его с помощью unpack в новую папку.
  3. Коммитим новую папку.
  4. Добавляем в файл из п. 1. изучаемый элемент (гиперссылку, таблицу и т.д.).
  5. Распаковываем изменённый файл в уже существующую папку.
  6. Изучаем diff, убирая ненужные изменения (перестановки связей, порядок пространств имён и т.п.).
  7. Запаковываем папку и проверяем что получившийся файл открывается.
  8. Коммитим изменённую папку.

Пример 1. Выделение текста жирным

Посмотрим на практике, как найти тег который определяет форматирование текста жирным шрифтом.

  1. Создаём документ bold.docx с обычным (не жирным) текстом Test.
  2. Распаковываем его: unpack bold.docx bold.
  3. Коммитим результат.
  4. Выделяем текст Test жирным.
  5. Распаковываем unpack bold.docx bold.
  6. Изначально diff выглядел следующим образом:

diff
Рассмотрим его подробно:

docProps/app.xml

@@ -1,9 +1,9 @@
-  <TotalTime>0</TotalTime>
+  <TotalTime>1</TotalTime>

Изменение времени нам не нужно.

docProps/core.xml

@@ -4,9 +4,9 @@
-  <cp:revision>1</cp:revision>
+  <cp:revision>2</cp:revision>
   <dcterms:created xsi:type="dcterms:W3CDTF">2017-02-07T19:37:00Z</dcterms:created>
-  <dcterms:modified xsi:type="dcterms:W3CDTF">2017-02-07T19:37:00Z</dcterms:modified>
+  <dcterms:modified xsi:type="dcterms:W3CDTF">2017-02-08T10:01:00Z</dcterms:modified>

Изменение версии документа и даты модификации нас также не интересует.

word/document.xml

diff

@@ -1,24 +1,26 @@
    <w:body>
-    <w:p w:rsidR="0076695C" w:rsidRPr="00290C70" w:rsidRDefault="00290C70">
+    <w:p w:rsidR="0076695C" w:rsidRPr="00F752CF" w:rsidRDefault="00290C70">
       <w:pPr>
         <w:rPr>
+          <w:b/>
           <w:lang w:val="en-US"/>
         </w:rPr>
       </w:pPr>
-      <w:r>
+      <w:r w:rsidRPr="00F752CF">
         <w:rPr>
+          <w:b/>
           <w:lang w:val="en-US"/>
         </w:rPr>
         <w:t>Test</w:t>
       </w:r>
       <w:bookmarkStart w:id="0" w:name="_GoBack"/>
       <w:bookmarkEnd w:id="0"/>
     </w:p>
-    <w:sectPr w:rsidR="0076695C" w:rsidRPr="00290C70">
+    <w:sectPr w:rsidR="0076695C" w:rsidRPr="00F752CF">

Изменения в w:rsidR не интересны — это внутренняя информация для Microsoft Word. Ключевое изменение тут

         <w:rPr>
+          <w:b/>

в параграфе с Test. Видимо элемент <w:b/> и делает текст жирным. Оставляем это изменение и отменяем остальные.

word/settings.xml

@@ -1,8 +1,9 @@
+  <w:proofState w:spelling="clean"/>
@@ -17,10 +18,11 @@
+    <w:rsid w:val="00F752CF"/>

Также не содержит ничего относящегося к жирному тексту. Отменяем.

7 Запаковываем папку с 1м изменением (добавлением <w:b/>) и проверяем что документ открывается и показывает то, что ожидалось.
8 Коммитим изменение.

Пример 2. Нижний колонтитул

Теперь разберём пример посложнее — добавление нижнего колонтитула.
Вот первоначальный коммит. Добавляем нижний колонтитул с текстом 123 и распаковываем документ. Такой diff получается первоначально:

diff

Сразу же исключаем изменения в docProps/app.xml и docProps/core.xml — там тоже самое, что и в первом примере.

[Content_Types].xml

@@ -4,10 +4,13 @@
   <Default Extension="xml" ContentType="application/xml"/>
   <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
+  <Override PartName="/word/footnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"/>
+  <Override PartName="/word/endnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"/>
+  <Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>

footer явно выглядит как то, что нам нужно, но что делать с footnotes и endnotes? Являются ли они обязательными при добавлении нижнего колонтитула или их создали заодно? Ответить на этот вопрос не всегда просто, вот основные пути:

  • Посмотреть, связаны ли изменения друг с другом
  • Экспериментировать
  • Ну а если совсем не понятно что происходит:

Читать документацию
Идём пока что дальше.

word/_rels/document.xml.rels

Изначально diff выглядит вот так:

diff

@@ -1,8 +1,11 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
   <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
+  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
   <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
   <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
-  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
-  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
+  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
+  <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" Target="endnotes.xml"/>
+  <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/>
 </Relationships>

Видно, что часть изменений связана с тем, что Word изменил порядок связей, уберём их:

@@ -3,6 +3,9 @@
+  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
+  <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" Target="endnotes.xml"/>
+  <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/>

Опять появляются footer, footnotes, endnotes. Все они связаны с основным документом, перейдём к нему:

word/document.xml

@@ -15,10 +15,11 @@
       </w:r>
       <w:bookmarkStart w:id="0" w:name="_GoBack"/>
       <w:bookmarkEnd w:id="0"/>
     </w:p>
     <w:sectPr w:rsidR="0076695C" w:rsidRPr="00290C70">
+      <w:footerReference w:type="default" r:id="rId6"/>
       <w:pgSz w:w="11906" w:h="16838"/>
       <w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="1701" w:header="708" w:footer="708" w:gutter="0"/>
       <w:cols w:space="708"/>
       <w:docGrid w:linePitch="360"/>
     </w:sectPr>

Редкий случай когда есть только нужные изменения. Видна явная ссылка на footer из sectPr. А так как ссылок в документе на footnotes и endnotes нет, то можно предположить что они нам не понадобятся.

word/settings.xml

diff

@@ -1,19 +1,30 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <w:settings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" mc:Ignorable="w14 w15">
   <w:zoom w:percent="100"/>
+  <w:proofState w:spelling="clean"/>
   <w:defaultTabStop w:val="708"/>
   <w:characterSpacingControl w:val="doNotCompress"/>
+  <w:footnotePr>
+    <w:footnote w:id="-1"/>
+    <w:footnote w:id="0"/>
+  </w:footnotePr>
+  <w:endnotePr>
+    <w:endnote w:id="-1"/>
+    <w:endnote w:id="0"/>
+  </w:endnotePr>
   <w:compat>
     <w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/>
     <w:compatSetting w:name="overrideTableStyleFontSizeAndJustification" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="enableOpenTypeFeatures" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="doNotFlipMirrorIndents" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="differentiateMultirowTableHeaders" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
   </w:compat>
   <w:rsids>
     <w:rsidRoot w:val="00290C70"/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:rsid w:val="001B0DE6"/>

А вот и появились ссылки на footnotes, endnotes добавляющие их в документ.

word/styles.xml

diff

@@ -480,6 +480,50 @@
       <w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
       <w:b/>
       <w:sz w:val="28"/>
     </w:rPr>
   </w:style>
+  <w:style w:type="paragraph" w:styleId="a4">
+    <w:name w:val="header"/>
+    <w:basedOn w:val="a"/>
+    <w:link w:val="a5"/>
+    <w:uiPriority w:val="99"/>
+    <w:unhideWhenUsed/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:pPr>
+      <w:tabs>
+        <w:tab w:val="center" w:pos="4677"/>
+        <w:tab w:val="right" w:pos="9355"/>
+      </w:tabs>
+      <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
+    </w:pPr>
+  </w:style>
+  <w:style w:type="character" w:customStyle="1" w:styleId="a5">
+    <w:name w:val="Верхний колонтитул Знак"/>
+    <w:basedOn w:val="a0"/>
+    <w:link w:val="a4"/>
+    <w:uiPriority w:val="99"/>
+    <w:rsid w:val="000A7B7B"/>
+  </w:style>
+  <w:style w:type="paragraph" w:styleId="a6">
+    <w:name w:val="footer"/>
+    <w:basedOn w:val="a"/>
+    <w:link w:val="a7"/>
+    <w:uiPriority w:val="99"/>
+    <w:unhideWhenUsed/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:pPr>
+      <w:tabs>
+        <w:tab w:val="center" w:pos="4677"/>
+        <w:tab w:val="right" w:pos="9355"/>
+      </w:tabs>
+      <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
+    </w:pPr>
+  </w:style>
+  <w:style w:type="character" w:customStyle="1" w:styleId="a7">
+    <w:name w:val="Нижний колонтитул Знак"/>
+    <w:basedOn w:val="a0"/>
+    <w:link w:val="a6"/>
+    <w:uiPriority w:val="99"/>
+    <w:rsid w:val="000A7B7B"/>
+  </w:style>
 </w:styles>

Изменения в стилях нас интересуют только если мы ищем как поменять стиль. В данном случае это изменение можно убрать.

Посмотрим теперь собственно на сам нижний колонтитул (часть пространств имён опущена для читабельности, но в документе они должны быть):

<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:p w:rsidR="000A7B7B" w:rsidRDefault="000A7B7B">
    <w:pPr>
      <w:pStyle w:val="a6"/>
    </w:pPr>
    <w:r>
      <w:t>123</w:t>
    </w:r>
  </w:p>
</w:ftr>

Тут виден текст 123. Единственное, что надо исправить — убрать ссылку на <w:pStyle w:val="a6"/>.

В результате анализа всех изменений делаем следующие предположения:

  • footnotes и endnotes не нужны
  • В [Content_Types].xml надо добавить footer
  • В word/_rels/document.xml.rels надо добавить ссылку на footer
  • В word/document.xml в тег <w:sectPr> надо добавить <w:footerReference>

Уменьшаем diff до этого набора изменений:

final diff

Затем запаковываем документ и открываем его.
Если всё сделано правильно, то документ откроется и в нём будет нижний колонтитул с текстом 123. А вот и итоговый коммит.

Таким образом процесс поиска изменений сводится к поиску минимального набора изменений, достаточного для достижения заданного результата.

Практика

Найдя интересующее нас изменение, логично перейти к следующему этапу, это может быть что-либо из:

  • Создания docx
  • Парсинг docx
  • Преобразования docx

Тут нам потребуются знания XSLT и XPath.

Давайте напишем достаточно простое преобразование — замену или добавление нижнего колонтитула в существующий документ. Писать я буду на языке Caché ObjectScript, но даже если вы его не знаете — не беда. В основном будем вызовать XSLT и архиватор. Ничего более. Итак, приступим.

Алгоритм

Алгоритм выглядит следующим образом:

  1. Распаковываем документ.
  2. Добавляем наш нижний колонтитул.
  3. Прописываем ссылку на него в [Content_Types].xml и word/_rels/document.xml.rels.
  4. В word/document.xml в тег <w:sectPr> добавляем тег <w:footerReference> или заменяем в нём ссылку на наш нижний колонтитул.
  5. Запаковываем документ.

Приступим.

Распаковка

В Caché ObjectScript есть возможность выполнять команды ОС с помощью функции $zf(-1, oscommand). Вызовем unzip для распаковки документа с помощью обёртки над $zf(-1):

/// Используя %3 (unzip) распаковать файл %1 в папку %2
Parameter UNZIP = "%3 %1 -d %2";

/// Распаковать архив source в папку targetDir
ClassMethod executeUnzip(source, targetDir) As %Status
{
    set timeout = 100
    set cmd = $$$FormatText(..#UNZIP, source, targetDir, ..getUnzip())
    return ..execute(cmd, timeout)
}

Создаём файл нижнего колонтитула

На вход поступает текст нижнего колонтитула, запишем его в файл in.xml:

<xml>TEST</xml>

В XSLT (файл — footer.xsl) будем создавать нижний колонтитул с текстом из тега xml (часть пространств имён опущена, вот полный список):

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns="http://schemas.openxmlformats.org/package/2006/relationships" version="1.0">
    <xsl:output method="xml" omit-xml-declaration="no" indent="yes" standalone="yes"/>
    <xsl:template match="/">

        <w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
            <w:p>
                <w:r>
                    <w:rPr>
                        <w:lang w:val="en-US"/>
                    </w:rPr>
                    <w:t>
                        <xsl:value-of select="//xml/text()"/>
                    </w:t>
                </w:r>
            </w:p>
        </w:ftr>
    </xsl:template>
</xsl:stylesheet>

Теперь вызовем XSLT преобразователь:

do ##class(%XML.XSLT.Transformer).TransformFile("in.xml", "footer.xsl", footer0.xml")    

В результате получится файл нижнего колонтитула footer0.xml:

<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
    <w:p>
        <w:r>
            <w:rPr>
                <w:lang w:val="en-US"/>
            </w:rPr>
            <w:t>TEST</w:t>
        </w:r>
    </w:p>
</w:ftr>

Добавляем ссылку на колонтитул в список связей основного документа

Сссылки с идентификатором rId0 как правило не существует. Впрочем можно использовать XPath для получения идентификатора которого точно не существует.
Добавляем ссылку на footer0.xml c идентификатором rId0 в word/_rels/document.xml.rels:

XSLT

<xsl:stylesheet  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns="http://schemas.openxmlformats.org/package/2006/relationships"  version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="no"  />
    <xsl:param name="new">
        <Relationship 
           Id="rId0" 
           Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" 
           Target="footer0.xml"/>
    </xsl:param>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:copy-of select="$new"/>
            <xsl:copy-of select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Прописываем ссылки в документе

Далее надо в каждый тег <w:sectPr> добавить тег <w:footerReference> или заменить в нём ссылку на наш нижний колонтитул. Оказалось, что у каждого тега <w:sectPr> может быть 3 тега <w:footerReference> — для первой страницы, четных страниц и всего остального:

XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />
    <xsl:template match="//@* | //node()">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="//w:sectPr">
        <xsl:element name="{name()}" namespace="{namespace-uri()}">
            <xsl:copy-of select="./namespace::*"/>
            <xsl:apply-templates select="@*"/>
            <xsl:copy-of select="./*[local-name() != 'footerReference']"/>
            <w:footerReference w:type="default" r:id="rId0"/>
            <w:footerReference w:type="first" r:id="rId0"/>
            <w:footerReference w:type="even" r:id="rId0"/>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Добавляем колонтитул в [Content_Types].xml

Добавляем в [Content_Types].xml информацию о том, что /word/footer0.xml имеет тип application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml:

XSLT

<xsl:stylesheet  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns="http://schemas.openxmlformats.org/package/2006/content-types"  version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="no"  />
    <xsl:param name="new">
        <Override 
         PartName="/word/footer0.xml" 
         ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
    </xsl:param>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:copy-of select="@* | node()"/> 
            <xsl:copy-of select="$new"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

В результате

Весь код опубликован. Работает он так:

do ##class(Converter.Footer).modifyFooter("in.docx", "out.docx", "TEST")

Где:

  • in.docx — исходный документ
  • out.docx — выходящий документ
  • TEST — текст, который добавляется в нижний колонтитул

Выводы

Используя только XSLT и ZIP можно успешно работать с документами docx, таблицами xlsx и презентациями pptx.

Открытые вопросы

  1. Изначально хотел использовать 7z вместо zip/unzip т… к. это одна утилита и она более распространена на Windows. Однако я столкнулся с такой проблемой, что документы запакованные 7z под Linux не открываются в Microsoft Office. Я попробовал достаточно много вариантов вызова, однако положительного результата добиться не удалось.
  2. Ищу XSD со схемами ECMA-376 версии 5 и комментариями. XSD версии 5 без комментариев доступен к загрузке на сайте ECMA, но без комментариев в нём сложно разобраться. XSD версии 2 с комментариями доступен к загрузке.

Ссылки

  • ECMA-376
  • Описание docx
  • Подробная статья про docx
  • Репозиторий со скриптами
  • Репозиторий с преобразователем нижнего колонтитула

Как я разбирал docx с помощью XSLT

Задача обработки документов в формате docx, а также таблиц xlsx и презентаций pptx является весьма нетривиальной. В этой статье расскажу как научиться парсить, создавать и обрабатывать такие документы используя только XSLT и ZIP архиватор.

Зачем?

docx — самый популярный формат документов, поэтому задача отдавать информацию пользователю в этом формате всегда может возникнуть. Один из вариантов решения этой проблемы — использование готовой библиотеки, может не подходить по ряду причин:

  • библиотеки может просто не существовать
  • в проекте не нужен ещё один чёрный ящик
  • ограничения библиотеки по платформам и т.п.
  • проблемы с лицензированием
  • скорость работы

Поэтому в этой статье будем использовать только самые базовые инструменты для работы с docx документом.

Структура docx

Для начала разоберёмся с тем, что собой представляет docx документ. docx это zip архив который физически содержит 2 типа файлов:

  • xml файлы с расширениями xml и rels
  • медиа файлы (изображения и т.п.)

А логически — 3 вида элементов:

  • Типы (Content Types) — список типов медиа файлов (например png) встречающихся в документе и типов частей документов (например документ, верхний колонтитул).
  • Части (Parts) — отдельные части документа, для нашего документа это document.xml, сюда входят как xml документы так и медиа файлы.
  • Связи (Relationships) идентифицируют части документа для ссылок (например связь между разделом документа и колонтитулом), а также тут определены внешние части (например гиперссылки).

Они подробно описаны в стандарте ECMA-376: Office Open XML File Formats, основная часть которого — PDF документ на 5000 страниц, и ещё 2000 страниц бонусного контента.

Минимальный docx

Простейший docx после распаковки выглядит следующим образом

image

Давайте посмотрим из чего он состоит.

[Content_Types].xml

Находится в корне документа и перечисляет MIME типы содержимого документа:

<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
    <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
    <Default Extension="xml" ContentType="application/xml"/>
    <Override PartName="/word/document.xml"
              ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>

_rels/.rels

Главный список связей документа. В данном случае определена всего одна связь — сопоставление с идентификатором rId1 и файлом word/document.xml — основным телом документа.

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship 
        Id="rId1" 
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
        Target="word/document.xml"/>
</Relationships>

word/document.xml

Основное содержимое документа.

<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:o="urn:schemas-microsoft-com:office:office"
            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
            xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
            xmlns:v="urn:schemas-microsoft-com:vml"
            xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
            xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
            xmlns:w10="urn:schemas-microsoft-com:office:word"
            xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
            xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
            xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
            xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
            xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
            xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
            mc:Ignorable="w14 wp14">
    <w:body>
        <w:p w:rsidR="005F670F" w:rsidRDefault="005F79F5">
            <w:r>
                <w:t>Test</w:t>
            </w:r>
            <w:bookmarkStart w:id="0" w:name="_GoBack"/>
            <w:bookmarkEnd w:id="0"/>
        </w:p>
        <w:sectPr w:rsidR="005F670F">
            <w:pgSz w:w="12240" w:h="15840"/>
            <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" 
                     w:header="720" w:footer="720" w:gutter="0"/>
            <w:cols w:space="720"/>
            <w:docGrid w:linePitch="360"/>
        </w:sectPr>
    </w:body>
</w:document>

Здесь:

  • <w:document> — сам документ
  • <w:body> — тело документа
  • <w:p> — параграф
  • <w:r> — run (фрагмент) текста
  • <w:t> — сам текст
  • <w:sectPr> — описание страницы

Если открыть этот документ в текстовом редакторе, то увидим документ из одного слова Test.

word/_rels/document.xml.rels

Здесь содержится список связей части word/document.xml. Название файла связей создаётся из названия части документа к которой он относится и добавления к нему расширения rels. Папка с файлом связей называется _rels и находится на том же уровне, что и часть к которой он относится. Так как связей в word/document.xml никаких нет то и в файле пусто:

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>

Даже если связей нет, этот файл должен существовать.

docx и Microsoft Word

docx созданный с помощью Microsoft Word, да в принципе и с помощью любого другого редактора имеет несколько дополнительных файлов.

image

Вот что в них содержится:

  • docProps/core.xml — основные метаданные документа согласно Open Packaging Conventions и Dublin Core [1], [2].
  • docProps/app.xml — общая информация о документе: количество страниц, слов, символов, название приложения в котором был создан документ и т.п.
  • word/settings.xml — настройки относящиеся к текущему документу.
  • word/styles.xml — стили применимые к документу. Отделяют данные от представления.
  • word/webSettings.xml — настройки отображения HTML частей документа и настройки того, как конвертировать документ в HTML.
  • word/fontTable.xml — список шрифтов используемых в документе.
  • word/theme1.xml — тема (состоит из цветовой схемы, шрифтов и форматирования).

В сложных документах частей может быть гораздо больше.

Реверс-инжиниринг docx

Итак, первоначальная задача — узнать как какой-либо фрагмент документа хранится в xml, чтобы потом создавать (или парсить) подобные документы самостоятельно. Для этого нам понадобятся:

  • Архиватор zip
  • Библиотека для форматирования XML (Word выдаёт XML без отступов, одной строкой)
  • Средство для просмотра diff между файлами, я буду использовать git и TortoiseGit

Инструменты

  • Под Windows: zip, unzip, libxml2, git, TortoiseGit
  • Под Linux: apt-get install zip unzip libxml2 libxml2-utils git

Также понадобятся скрипты для автоматического (раз)архивирования и форматирования XML.
Использование под Windows:

  • unpack file dir — распаковывает документ file в папку dir и форматирует xml
  • pack dir file — запаковывает папку dir в документ file

Использование под Linux аналогично, только ./unpack.sh вместо unpack, а pack становится ./pack.

Использование

Поиск изменений происходит следующим образом:

  1. Создаём пустой docx файл в редакторе.
  2. Распаковываем его с помощью unpack в новую папку.
  3. Коммитим новую папку.
  4. Добавляем в файл из п. 1. изучаемый элемент (гиперссылку, таблицу и т.д.).
  5. Распаковываем изменённый файл в уже существующую папку.
  6. Изучаем diff, убирая ненужные изменения (перестановки связей, порядок пространств имён и т.п.).
  7. Запаковываем папку и проверяем что получившийся файл открывается.
  8. Коммитим изменённую папку.

Пример 1. Выделение текста жирным

Посмотрим на практике, как найти тег который определяет форматирование текста жирным шрифтом.

  1. Создаём документ bold.docx с обычным (не жирным) текстом Test.
  2. Распаковываем его: unpack bold.docx bold.
  3. Коммитим результат.
  4. Выделяем текст Test жирным.
  5. Распаковываем unpack bold.docx bold.
  6. Изначально diff выглядел следующим образом:

diff
Рассмотрим его подробно:

docProps/app.xml

@@ -1,9 +1,9 @@
-  <TotalTime>0</TotalTime>
+  <TotalTime>1</TotalTime>

Изменение времени нам не нужно.

docProps/core.xml

@@ -4,9 +4,9 @@
-  <cp:revision>1</cp:revision>
+  <cp:revision>2</cp:revision>
   <dcterms:created xsi:type="dcterms:W3CDTF">2017-02-07T19:37:00Z</dcterms:created>
-  <dcterms:modified xsi:type="dcterms:W3CDTF">2017-02-07T19:37:00Z</dcterms:modified>
+  <dcterms:modified xsi:type="dcterms:W3CDTF">2017-02-08T10:01:00Z</dcterms:modified>

Изменение версии документа и даты модификации нас также не интересует.

word/document.xml

«`diff
@@ -1,24 +1,26 @@


+

+


+

+

Test


+
«`

Изменения в w:rsidR не интересны — это внутренняя информация для Microsoft Word. Ключевое изменение тут

в параграфе с Test. Видимо элемент <w:b/> и делает текст жирным. Оставляем это изменение и отменяем остальные.

word/settings.xml

@@ -1,8 +1,9 @@
+  <w:proofState w:spelling="clean"/>
@@ -17,10 +18,11 @@
+    <w:rsid w:val="00F752CF"/>

Также не содержит ничего относящегося к жирному тексту. Отменяем.

7 Запаковываем папку с 1м изменением (добавлением <w:b/>) и проверяем что документ открывается и показывает то, что ожидалось.
8 Коммитим изменение.

Пример 2. Нижний колонтитул

Теперь разберём пример посложнее — добавление нижнего колонтитула.
Вот первоначальный коммит. Добавляем нижний колонтитул с текстом 123 и распаковываем документ. Такой diff получается первоначально:

diff

Сразу же исключаем изменения в docProps/app.xml и docProps/core.xml — там тоже самое, что и в первом примере.

[Content_Types].xml

@@ -4,10 +4,13 @@
   <Default Extension="xml" ContentType="application/xml"/>
   <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
+  <Override PartName="/word/footnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"/>
+  <Override PartName="/word/endnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"/>
+  <Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>

footer явно выглядит как то, что нам нужно, но что делать с footnotes и endnotes? Являются ли они обязательными при добавлении нижнего колонтитула или их создали заодно? Ответить на этот вопрос не всегда просто, вот основные пути:

  • Посмотреть, связаны ли изменения друг с другом
  • Экспериментировать
  • Ну а если совсем не понятно что происходит:

Читать документацию
Идём пока что дальше.

word/_rels/document.xml.rels

Изначально diff выглядит вот так:

«`diff
@@ -1,8 +1,11 @@

+

+



+
+
+

«`

Видно, что часть изменений связана с тем, что Word изменил порядок связей, уберём их:
«`diff
@@ -3,6 +3,9 @@
+
+
+
«`
Опять появляются footer, footnotes, endnotes. Все они связаны с основным документом, перейдём к нему:

word/document.xml

@@ -15,10 +15,11 @@
       </w:r>
       <w:bookmarkStart w:id="0" w:name="_GoBack"/>
       <w:bookmarkEnd w:id="0"/>
     </w:p>
     <w:sectPr w:rsidR="0076695C" w:rsidRPr="00290C70">
+      <w:footerReference w:type="default" r:id="rId6"/>
       <w:pgSz w:w="11906" w:h="16838"/>
       <w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="1701" w:header="708" w:footer="708" w:gutter="0"/>
       <w:cols w:space="708"/>
       <w:docGrid w:linePitch="360"/>
     </w:sectPr>

Редкий случай когда есть только нужные изменения. Видна явная ссылка на footer из sectPr. А так как ссылок в документе на footnotes и endnotes нет, то можно предположить что они нам не понадобятся.

word/settings.xml

@@ -1,19 +1,30 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <w:settings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" mc:Ignorable="w14 w15">
   <w:zoom w:percent="100"/>
+  <w:proofState w:spelling="clean"/>
   <w:defaultTabStop w:val="708"/>
   <w:characterSpacingControl w:val="doNotCompress"/>
+  <w:footnotePr>
+    <w:footnote w:id="-1"/>
+    <w:footnote w:id="0"/>
+  </w:footnotePr>
+  <w:endnotePr>
+    <w:endnote w:id="-1"/>
+    <w:endnote w:id="0"/>
+  </w:endnotePr>
   <w:compat>
     <w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/>
     <w:compatSetting w:name="overrideTableStyleFontSizeAndJustification" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="enableOpenTypeFeatures" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="doNotFlipMirrorIndents" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="differentiateMultirowTableHeaders" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
   </w:compat>
   <w:rsids>
     <w:rsidRoot w:val="00290C70"/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:rsid w:val="001B0DE6"/>

А вот и появились ссылки на footnotes, endnotes добавляющие их в документ.

word/styles.xml

«`diff
@@ -480,6 +480,50 @@

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

«`

Изменения в стилях нас интересуют только если мы ищем как поменять стиль. В данном случае это изменение можно убрать.

word/footer1.xml

Посмотрим теперь собственно на сам нижний колонтитул (часть пространств имён опущена для читабельности, но в документе они должны быть):

<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:p w:rsidR="000A7B7B" w:rsidRDefault="000A7B7B">
    <w:pPr>
      <w:pStyle w:val="a6"/>
    </w:pPr>
    <w:r>
      <w:t>123</w:t>
    </w:r>
  </w:p>
</w:ftr>

Тут виден текст 123. Единственное, что надо исправить — убрать ссылку на <w:pStyle w:val="a6"/>.

В результате анализа всех изменений делаем следующие предположения:

  • footnotes и endnotes не нужны
  • В [Content_Types].xml надо добавить footer
  • В word/_rels/document.xml.rels надо добавить ссылку на footer
  • В word/document.xml в тег <w:sectPr> надо добавить <w:footerReference>

Уменьшаем diff до этого набора изменений:

final diff

Затем запаковываем документ и открываем его.
Если всё сделано правильно, то документ откроется и в нём будет нижний колонтитул с текстом 123. А вот и итоговый коммит.

Таким образом процесс поиска изменений сводится к поиску минимального набора изменений, достаточного для достижения заданного результата.

Практика

Найдя интересующее нас изменение, логично перейти к следующему этапу, это может быть что-либо из:

  • Создания docx
  • Парсинг docx
  • Преобразования docx

Тут нам потребуются знания XSLT и XPath.

Давайте напишем достаточно простое преобразование — замену или добавление нижнего колонтитула в существующий документ. Писать я буду на языке Caché ObjectScript, но даже если вы не знаете — не беда. В основном будем вызовать XSLT и архиватор. Ничего более. Итак, приступим.

Алгоритм

Алгоритм выглядит следующим образом:

  1. Распаковываем документ
  2. Добавляем наш нижний колонтитул
  3. Прописываем ссылку на него в [Content_Types].xml и word/_rels/document.xml.rels
  4. В word/document.xml в тег <w:sectPr> добавляем тег <w:footerReference> или заменяем в нём ссылку на наш нижний колонтитул.
  5. Запаковываем документ

Приступим.

Распаковка

В Caché ObjectScript есть возможность выполнять команды ОС с помощью функции $zf(-1, oscommand). Вызовем unzip для распаковки документа с помощью обёртки над $zf(-1):

/// Используя %3 (unzip) распаковать файл %1 в папку %2
Parameter UNZIP = "%3 %1 -d %2";

/// Распаковать архив source в папку targetDir
ClassMethod executeUnzip(source, targetDir) As %Status
{
    set timeout = 100
    set cmd = $$$FormatText(..#UNZIP, source, targetDir, ..getUnzip())
    return ..execute(cmd, timeout)
}

Создаём файл нижнего колонтитула

На вход поступает текст нижнего колонтитула, запишем его в файл in.xml:

В XSLT (файл — footer.xsl) будем создавать нижний колонтитул с текстом из тега xml (часть пространств имён опущена, вот полный список):

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns="http://schemas.openxmlformats.org/package/2006/relationships" version="1.0">
    <xsl:output method="xml" omit-xml-declaration="no" indent="yes" standalone="yes"/>
    <xsl:template match="/">

        <w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
            <w:p>
                <w:r>
                    <w:rPr>
                        <w:lang w:val="en-US"/>
                    </w:rPr>
                    <w:t>
                        <xsl:value-of select="//xml/text()"/>
                    </w:t>
                </w:r>
            </w:p>
        </w:ftr>
    </xsl:template>
</xsl:stylesheet>

Теперь вызовем XSLT преобразователь:

do ##class(%XML.XSLT.Transformer).TransformFile("in.xml", "footer.xsl", footer0.xml")    

В результате получится файл нижнего колонтитула footer0.xml:

<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
    <w:p>
        <w:r>
            <w:rPr>
                <w:lang w:val="en-US"/>
            </w:rPr>
            <w:t>TEST</w:t>
        </w:r>
    </w:p>
</w:ftr>

Добавляем ссылку на колонтитул в список связей основного документа

Сссылки с идентификатором rId0 как правило не существует. Впрочем можно использовать XPath для получения идентификатора которого точно не существует.
Добавляем ссылку на footer0.xml c идентификатором rId0 в word/_rels/document.xml.rels:

«`xml

<xsl:template match="/*">
    <xsl:copy>
        <xsl:copy-of select="$new"/>
        <xsl:copy-of select="@* | node()"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

</spoiler>

#### Прописываем ссылки в документе

Далее надо в каждый тег `<w:sectPr>` добавить тег `<w:footerReference>` или заменить в нём ссылку на наш нижний колонтитул. [Оказалось](https://msdn.microsoft.com/en-us/library/documentformat.openxml.wordprocessing.footerreference(v=office.14).aspx), что у каждого тега `<w:sectPr>` может быть 3 тега `<w:footerReference>` - для первой страницы, четных страниц и всего остального:
<spoiler title="XSLT">
```xml
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />
    <xsl:template match="//@* | //node()">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="//w:sectPr">
        <xsl:element name="{name()}" namespace="{namespace-uri()}">
            <xsl:copy-of select="./namespace::*"/>
            <xsl:apply-templates select="@*"/>
            <xsl:copy-of select="./*[local-name() != 'footerReference']"/>
            <w:footerReference w:type="default" r:id="rId0"/>
            <w:footerReference w:type="first" r:id="rId0"/>
            <w:footerReference w:type="even" r:id="rId0"/>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Добавляем колонтитул в [Content_Types].xml

Добавляем в [Content_Types].xml информацию о том, что /word/footer0.xml имеет тип application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml:

«`xml

<xsl:template match="/*">
    <xsl:copy>
        <xsl:copy-of select="@* | node()"/> 
        <xsl:copy-of select="$new"/>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

</spoiler>


#### В результате

Весь код [опубликован](https://github.com/intersystems-ru/Converter/blob/master/Converter/Footer.cls.xml). Работает он так:
```cos
do ##class(Converter.Footer).modifyFooter("in.docx", "out.docx", "TEST")

Где:

  • in.docx — исходный документ
  • out.docx — выходящий документ
  • TEST — текст, который добавляется в нижний колонтитул

Выводы

Используя только XSLT и ZIP можно успешно работать с документами docx, таблицами xlsx и презентациями pptx.

Открытые вопросы

  1. Изначально хотел использовать 7z вместо zip/unzip т..к. это одна утилита и она более распространена на Windows. Однако я столкнулся с такой проблемой, что документы запакованные 7z под Linux не открываются в Microsoft Office. Я попробовал достаточно много вариантов вызова, однако положительного результата добиться не удалось.
  2. Ищу XSD со схемами ECMA-376 версии 5 и комментариями. XSD версии 5 без комментариев доступен к загрузке на сайте ECMA, но без комментариев в нём сложно разобраться. XSD версии 2 с комментариями доступен к загрузке.

Ссылки

  • ECMA-376
  • Описание docx
  • Подробная статья про docx
  • Репозиторий со скриптами
  • Репозиторий с преобразователем нижнего колонтитула
  • Download source — 73.4 KB

Introduction (ramble)

Another boring day in my life. I often wonder why it has to be like this – I either have no obligations at all, or I’m packed with them. Most people tell me that it has to do with planning. They wear their favorite serious face and say: you, my son, just don’t know how to properly make your schedule. Time is the essence, do not use it improperly! You must divide your time in million little pieces, label each one of them, implement some kind of sorting, import everything into Microsoft Outlook, and stick to that plan. Then, and only then, you’ll be a man who is not a campaigner, but an organized, equally time pressured «snooze-dismiss» monkey individual.

My answer is way simpler, I often reply with just – hey, screw you! ;)

Because, really, I’ve always believed it’s not about planning, but about the way universe has been built. Think about it – everything important was created in just those few seconds after the Big Bang. All that followed was just plain simple boring processes of waiting for the fruits of planted seeds to grow; the stage setting for another big moment.

So, I hope you’ll agree that it is – as Derek Ager once wrote – like the life of a soldier… long periods of boredom, and short periods of terror. One can just hope that those «long periods of boredom» can be filled with small joys of doing something you like; something that’ll ease your wait for those important «short periods».

This article is just that, my way of getting along with boredom… if it helps someone else, or fills his spare time, my joy will only be greater.

Index

  • Problem
  • Brief solution description
  • Producing the XSL transformation
    • Defining the XML schema based on the report
    • Binding data from the Word document to the appropriate fields in the XML schema
    • Saving into WordML and the generation of XSLT
    • Solving problems with multiple used elements
    • Inserting images into the document
    • Opening the document in read-only mode
  • Preparing data and applying the transformation
    • T-SQL and XML
    • Binding XML to schema
  • Applying the transformation on XML data
  • XML->XSLT->HTML->Word, the easy way out
  • Organization of resources used for the generation in the Visual Studio project
  • FAQ
  • Conclusion
  • References
  • History

Problem

I don’t know if you are in a club, but I’ve met numerous .NET developers who had much trouble with choosing the right tool to build reports. Apart from praise for the Access report building capabilities, you won’t hear many compliments for the reporting tools.

I guess we have all tried Crystal Reports embedded into Visual Studio .NET — they are OK, but are demanding. And often, small bugs, along with ridiculous option placements, will drive you nuts.

SQL Reporting Services are somewhat a new option that is praised all over the web by Microsoft evangelists. In practice, however, I’ve often stumbled on projects where the team is paralyzed with problems concerning configuration and specific aspects of report writing.

Finally, there are numerous custom reporting frameworks such as ActiveReports or DevExpress’ (I love these guys) Reporting Tools.

Specific maladies aside, the common problem with all the previously laid options is that they have a modest learning curve. I’m not talking about the time needed to acquire the knowledge for generating a list of employees from an «It’s easy to use our report suite»™ example. I’m talking about the time needed to acquire the knowledge for developing real-life reports which have three tables that properly expand and contract (along with its columns and rows) over pages.

Also, none of these options provide you with the solution for frequent user requirements – when a report is rendered, it should be possible to modify it a bit. The workaround is to use report exporting to popular formats that are known to most users, like Word.

As I’ve experienced, this is the point when the bulb shines above the head of the developer and the idea comes — why not generate reports in Word in the first place. In the majority of projects, clients are provided with the needed output reports in Word format, which they print and fill by hand. And if not… well, you have one of the best «report designers» in the world, as it was tweaked and improved over numerous versions.

So, how to do it?

Brief solution description

One big, big problem with Word documents before the 2003 version was their binary format. Word’s file format was not publicly available, and all utilities that could parse it were mostly developed by reverse-engineering, or by stealing using documentation available to Microsoft partners. You can guess that results weren’t too satisfying…

However, in 2003, Microsoft introduced XML formats for storing Office documents. Those formats were succeeded by Office Open XML formats in Office 2007 (which are default, instead of their binary counterparts), so you can safely bet that they are here to stay.

So, in order to generate a Word file now, you basically need to apply the appropriate XSLT (XSL Transform) onto the XML data used in a report. This process can be divided into several operational steps:

  1. Defining the XML schema based on the report
  2. Binding data from the Word document to the appropriate fields in XML schema
  3. Saving the Word document in WordML format and the generation of XSLT using the WML2XSLT tool
  4. Retrieving the needed data from a source (mostly a SQL Server database), it’s structuring into appropriate XML
  5. Applying XSLT onto XML data in order to generate the Word document, which then can be further manipulated (sending over wire, displaying to user, and similar)

The biggest problem is to produce valid XSLT; from five steps, three are taken to do that. The generation of XML is far easier, while the transformation is completely trivial.

Producing the XSL transformation

Defining the XML schema based on the report

In order to start making the report, it is required to define the necessary data. A picture talks more than a thousand words, an example talks almost an equal amount… so let’s look at the picture of the report that we’ll use as an example:

Figure 1 – Report that should be generated

Figure 1 – Report that should be generated

It is obvious that we first have the buyer’s name, the document date follows. Then we have, from the developer point of view, an interesting table of invoice items… and so on. The structure of the XML which will hold this data is described using an XML schema. Visual Studio 2005 has nice support for visual design of schemas, which we will utilize – after starting the IDE, take option File –> New –> File (CTRL+N): this gives a list of possible document types from which we choose XML Schema.

An element from the Toolbox should then be dragged-and-dropped on the workspace and filled with content. This process is shown on the picture that follows:

Figure 2 – Schema that defines structure of data for report

Figure 2 – Schema that defines the structure of data for the report

In order to be properly mapped, items on the invoice need to be described as child elements of the Invoice entity. Add -> New element from the context menu shown after right click gives the option to perform this action.

Figure 3 – Adding child to Invoice entity

Figure 3 – Adding a child to the Invoice entity

Adding the rest of the elements, assigning types to variables, and setting the targetNamespace (in the Properties window) gets the job done.

Assigning types to variables is optional in most cases – if you use special formats for printing out documents (like dd.MM.yyyy) or monetary values ($10.99), it’s easier to leave everything in the schema in string type, and do the formatting and validation during the generation of XML with the data.

On the other hand, setting the targetNamespace shouldn’t be optional – the produced schema will get the default value http://tempuri.org/XMLSchema.xsd. We can put aside the rules of good practice that tells us not to use the http://tempuri.org/ namespace in production; but, if you don’t give unique names to your schemas, you’ll stumble into problems during import and usage – Word’s schema library can’t hold two different schemas with the same namespace. So, be sure to set the targetNamespace (the convention http://Organization/Project/SchemaName.xsd is used mostly) before you close the definition.

Figure 4 – Resulting XML schema

Figure 4 – Resulting XML schema

Binding data from the Word document to the appropriate fields in the XML schema

Schema importing is performed by using the XML Structure dialog. In the 2003 version of Office Word, this dialog is accessible through Task Pane (CTRL+F1); it should be chosen from the list shown when clicked on the triangle in the header (left from the small x). If schemas aren’t previously imported, and the Template and Add-Ins option is chosen, the picture that follows will faithfully resemble the resulting state of the screen.

Figure 5 – Adding new XML Schema in Word document

Figure 5 – Adding the new XML schema in the Word document

In the dialog shown after clicking on the Add Schema button, it is needed to point to the location of the defined XML schema. Its fields will be then shown in the XML Structure dialog, from where they are further bound to the document data. Before starting that sweet job, some additional options should be set:

  • Check Ignore mixed content – This allows mixing data from the XML with data from the document. As documents are almost always made of fixed and variable parts, this avoids frequent signalization by Word that between the data defined in the XML schema there are «some others that don’t belong there».
  • Check Show advanced XML error messages – Choosing developer-friendly messages over user-friendly ones.
  • Check Allow saving as XML even if not valid – Most often, you just can’t «validly» mark data in the report document. For example, if some data from the XML is used twice in the document, Word will signal error in validation because according to the XML schema, that data appears only once. The same problem happens with order.
  • This is present to force valid entry of data in the Word document (another application of the technique that is being described). However, our current goal is diametrically opposite – we are not marking fields for entry, but for space in which data from the XML will be inserted, so it’s not needed to force a unique appearance and order.

Figure 6 – Dialog for setting XML data

Figure 6 – Dialog for setting XML data

After the schema is imported in to the document and the options set, it’s time to move onto binding the schema and the data. Initially, only the root element (in our case, Invoice) is available. After choosing it, Word will offer options for assigning the schema to the appropriate range in the document.

Figure 7 – Options for applying schema on appropriate range in document

Figure 7 – Options for applying the schema on the appropriate range in the document

In this example, applying the schema to the entire document is a needed option (possible multi-schematic Word files aren’t interesting from the reporting point of view). Now, what is left is to mark the data – the selected text is bound to the schema either by choosing the field from the Task Pane, or by using the option Apply XML Element shown after a right click.

Figure 8 – Binding data from Word document to fields of XML schema

Figure 8 – Binding data from a Word document to fields of the XML schema

Two things are interesting here. First, to define child items, you need to select and map the whole row in the table to the InvoiceItems element, after which Name and Price will be available for bounding to the cell’s data. If the document contains a large number of items, there is no need to map every single row; mapping just the first row is fine, the rest can be deleted. The structure of report, not the content, is what matters at the moment.

Second, Word, for previously explained reasons, signals error for double usage of the Buyer element (look at the picture). It’ll cause problems later, during the generation of the XSLT, but we can omit that problem for now (if Allow saving as XML even if not valid is checked in the XML options).

Saving into WordML and the generation of XSLT

The marked document contains all the data needed for the generation of valid XSLT. The WML2XSLT tool accepts WordML as input, so it’s required to save the Word document in this format. You can do this by using the Save As option from the File menu – when the dialog is shown in Save as Type, choose XML document (*.xml). The option Apply transform is used in the opposite direction, Data only when XML data is fetched from the document, so both fields should be left unchecked.

The prepared WML file is processed using this statement in the Command Prompt (the following is valid assuming that everything is in the same directory):

WML2XSLT.exe "WordGeneratedInvoice.xml" –o "WordGeneratedInvoice.xslt"

In case you run into problems (FileNotFoundException) while using the WML2XSLT.exe packed with the article source, be sure to download the tool from the previously given link and perform the installation (as mobfigr noted in his comment).

Solving problems with multiple used elements

The generated XSL transform will almost always be satisfying. One exception is when an element from the XML with data is used multiple times. In the example we are developing, the Buyer element is used twice, and for its second appearance, the following will be generated (you need to open the XSLT in Notepad or Visual Studio .NET and search for the value ns1:Buyer):

<w:r> <w:t><xsl:text>(Buyer: </xsl:text></w:t></w:r>
<xsl:apply-templates select="ns1:Buyer[position() >= 2]" />
<w:r> <w:t><xsl:text>)</xsl:text></w:t></w:r>

It’s obvious we aren’t interested in the element Buyer on the second position, but the same one that is referenced earlier in the file. Because of that, the following correction should be made:

<w:r> <w:t><xsl:text>(Buyer: </xsl:text></w:t></w:r>
<xsl:apply-templates select="ns1:Buyer" />
<w:r> <w:t><xsl:text>)</xsl:text></w:t></w:r>

Inserting images into the document

Naturally, WordML has good support for images, but it is very poorly documented. So, in order to see how images are represented in WML format, we’ll perform a little experiment and save the marked Word document displayed below as XML:

Figure 9 – Document with image

Figure 9 – Document with image

After processing the saved document using the WML2XML tool (with the WML2XML ExampleImage.xml -o ExampleImage.xslt command), and opening the generated XSLT file, we can scroll to the SomeImage tag and see the following:

<ns0:SomeImage>
  <xsl:for-each select="@ns0:*|@*[namespace-uri()='']">
    <xsl:attribute name="{name()}" namespace="{namespace-uri()}">
      <xsl:value-of select="." />
    </xsl:attribute>
  </xsl:for-each>
  <w:r>
    <w:pict>
      <v:shapetype id="_x0000_t75" coordsize="21600,21600" o:spt="75" 
              o:preferrelative="t" path="m@4@5l@4@11@9@11@9@5xe" filled="f" stroked="f">
        <v:stroke joinstyle="miter" />
        <v:formulas>
          <v:f eqn="if lineDrawn pixelLineWidth 0" />
          <v:f eqn="sum @0 1 0" />
          <v:f eqn="sum 0 0 @1" />
          <v:f eqn="prod @2 1 2" />
          <v:f eqn="prod @3 21600 pixelWidth" />
          <v:f eqn="prod @3 21600 pixelHeight" />
          <v:f eqn="sum @0 0 1" />
          <v:f eqn="prod @6 1 2" />
          <v:f eqn="prod @7 21600 pixelWidth" />
          <v:f eqn="sum @8 21600 0" />
          <v:f eqn="prod @7 21600 pixelHeight" />
          <v:f eqn="sum @10 21600 0" />
        </v:formulas>
        <v:path o:extrusionok="f" gradientshapeok="t" o:connecttype="rect" />
        <o:lock v:ext="edit" aspectratio="t" />
      </v:shapetype>
      <w:binData w:name="wordml://01000001.gif">R0lGODlhEAAQAPIGAAAAAAAAsACwALAAALD/sP+wsP
   ///////yH5BAEAAAcALAAAAAAQABAAAAOW
eHd3h3d3d3h3d4d3cHd4d3eHd3cHWHAXgXF3d3gHVYNwZxZ4d3eAVTUDeHdhh3d3UFgDdocRcXd4
d1CAdncXaHZ3h3dgd3h3Z4d3d3d4d3eHB3d3eHd3h3d3QAh3d4d3d3d4QCSAd3d3eHcHhEQicHh3
d4d3B0QoYHeHd3d3eAcEhnd3d3h3d4cHdnd4d3eHd3d3eHeXADu=
</w:binData>
      <v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:12pt;height:12pt">
        <v:imagedata src="wordml://01000001.gif" o:title="convert" />
      </v:shape>
    </w:pict>
  </w:r>
  <w:p>
    <w:r>
      <w:t>
        <xsl:value-of select="." />
      </w:t>
    </w:r>
  </w:p>
</ns0:SomeImage>

Obviously, the image is Base64 encoded into the XML file between the <w:binData> tags. After that, we have the <v:shape> tag which defines the placing of the image and references the encoded binary data by using <v:imagedata>. All this is preceded by <v:shapetype>, which is (luckily) optional and can be removed. Now, when we have some understanding of the format, we can perform a little clean up and properly place xsl:value-of select, so that binary data comes from our XML file:

<ns0:SomeImage>
  <xsl:for-each select="@ns0:*|@*[namespace-uri()='']">
    <xsl:attribute name="{name()}" namespace="{namespace-uri()}">
      <xsl:value-of select="." />
    </xsl:attribute>
  </xsl:for-each>
  <w:r>
    <w:pict>
      <w:binData w:name="wordml://01000001.gif"><xsl:value-of select="." /></w:binData>
      <v:shape id="_x0000_i1025" type="#_x0000_t75" style="width:12pt;height:12pt">
        <v:imagedata src="wordml://01000001.gif" o:title="convert" />
      </v:shape>
    </w:pict>
  </w:r>
</ns0:SomeImage>

It looks better, doesn’t it? All that is left is to supply the XML data in the proper format:

="1.0"="utf-8"
<Something xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/ImageExample.xsd">
    <SomeText>Small image below</SomeText>
    <SomeImage>R0lGODlhE[-- binary data truncated --]3d3eHeXADu=</SomeImage>
</Something>

and we’ll have the document from Figure 9 in no time. One final word of warning — if your images aren’t always the same size, you’ll want to check the style attribute of the <v:shape> tag. And, after checking, you’ll probably want to move it out of the transformation into XML ;). Here is how to do that:

<w:pict>
  <w:binData w:name="wordml://01000001.gif">
      <xsl:value-of select="." />
  </w:binData>
    <v:shape id="_x0000_i1025" type="#_x0000_t75">
        <xsl:attribute name="style">
            <xsl:value-of select="@style"/>
        </xsl:attribute>
        <v:imagedata src="wordml://01000001.gif" o:title="convert" />
    </v:shape>
</w:pict>
 
="1.0"="utf-8"
<Something xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/ImageExample.xsd">
    <SomeText>Small image below</SomeText>
    <SomeImage style="width:24pt;height:24pt">R0lGOD[-- binary data truncated --]3d3eADu=
    </SomeImage>
</Something>

Opening the document in read-only mode

To force opening the report in read-only mode when the report is displayed to the user, it’s needed to use the Tools -> Options -> Security -> Protect Document option during the document creation. Under Editing Restrictions, ‘No changes (Read only)’ should be chosen… after that, the only thing left to do is click onto ‘Yes, Start Enforcing Protection’ and enter the password for protection. Of course, further steps remain the same — the document is saved as WordML, processed through the WML2XSLT tool…

Figure 9 – Settings for read-only mode

Figure 10 – Settings for the read-only mode

Do not expect too much from this «protection». In WordML format, it’s enforced by one line in the DocumentProperties element:

<w:docPr>
  <w:view w:val="print" />
  <w:zoom w:percent="85" />
  <w:doNotEmbedSystemFonts />
  <w:proofState w:spelling="clean" w:grammar="clean" />
  <w:attachedTemplate w:val="" />
  <u><w:documentProtection w:edit="read-only" w:enforcement="on"
                           w:unprotectPassword="4560CA9C" /></u>
  <w:defaultTabStop w:val="720" />
  <w:punctuationKerning />
  <w:characterSpacingControl w:val="DontCompress" />
  <w:optimizeForBrowser />
  <w:validateAgainstSchema />
  <w:saveInvalidXML />
  <w:ignoreMixedContent />
  <w:alwaysShowPlaceholderText w:val="off" />
  <w:compat>
    <w:breakWrappedTables />
    <w:snapToGridInCell />
    <w:wrapTextWithPunct />
    <w:useAsianBreakRules />
    <w:dontGrowAutofit />
  </w:compat>
  <w:showXMLTags w:val="off" />
</w:docPr>

This means that the read-only mode can be easily incorporated into XSLT for reports you’ve already done… but, it also means that anyone knowing WML format can easily workaround your «protection». So, use it wisely :)

Preparing data and applying the transformation

T-SQL and XML

XML data that satisfies the previously defined schema and which we’ll use in the report can be generated in many ways. The most commonly used is the one that utilizes the SELECT... FOR XML command and data from SQL Server 2005 that directly translates into XML.

SELECT... FOR XML has two parameters:

  1. Work mode, chosen from RAW, AUTO, EXPLICIT, and the PATH array. In general, the AUTO mode will finish the job; when extra formatting is needed, the PATH mode is the choice.
  2. Additional variables like ROOT (add a root tag to XML), ELEMENTS (format output data as elements), TYPE (result is returned as XML type of SQL Server 2005), and XMLSCHEMA (write XML schema before data).

For example, if there is a c_City table with columns CityId and CityName, and XML with element City is needed, the following T-SQL is required:

SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO

<City CityId="43" CityName="100 Mile House" />
<City CityId="53" CityName="Abbotsford" />

If it’s needed to write out data in elements, the ELEMENTS directive is added:

SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO, ELEMENTS

<City>
  <CityId>43</CityId>
  <CityName>100 Mile House</CityName>
</City>
<City>
  <CityId>53</CityId>
  <CityName>Abbotsford</CityName>
</City>

As two elements exist on the first level, Root tag must be added so that the XML is syntactically valid:

SELECT CityId, CityName FROM c_City AS City
FOR XML AUTO, ELEMENTS, ROOT('Root')

<Root>
  <City>
    <CityId>43</CityId>
    <CityName>100 Mile House</CityName>
  </City>
  <City>
    <CityId>53</CityId>
    <CityName>Abbotsford</CityName>
  </City>
</Root>

Let’s assume that there is a c_PostalCode table with postal codes used in cities. If it’s required to make XML where postal codes will be child element of cities, the following SQL is in order:

SELECT CityId, CityName,  
    (SELECT PostalCodeId, PostalCodeName FROM c_PostalCode
     WHERE CityId = City.CityId
     FOR XML AUTO, TYPE)        
FROM c_City AS City
FOR XML AUTO, TYPE

<Root>
  <City CityId="43" CityName="100 Mile House">
    <c_PostalCode PostalCodeId="317701" PostalCodeName="V0K2Z0" />
    <c_PostalCode PostalCodeId="317702" PostalCodeName="V0K2E0" />
  </City>
  <City CityId="53" CityName="Abbotsford">
    <c_PostalCode PostalCodeId="317703" PostalCodeName="V3G2J3" />
  </City>
</Root>

If more output flexibility is required, it’s possible to format the XML in more detail using the PATH mode. For example, if it’s needed to hold CityId as an attribute, CityName as an element, and information about postal codes as child elements which PostalCodeId places in the NotNeeded sub element, use this T-SQL:

SELECT CityId AS '@CityId', CityName,  
    (SELECT PostalCodeId AS 'NotNeeded/PostalCodeId', PostalCodeName 
     FROM c_PostalCode
     WHERE CityId = City.CityId
     FOR XML path('PostalCode'), TYPE)        
FROM c_City AS City
FOR XML PATH('CityRow'), type, root('Data')

<Data>
  <CityRow CityId="43">
    <CityName>100 Mile House</CityName>
    <PostalCode PostalCodeName="V0K2Z0">
      <NotNeeded>
        <PostalCodeId>317701</PostalCodeId>
      </NotNeeded>
    </PostalCode>
    <PostalCode PostalCodeName="V0K2E0">
      <NotNeeded>
        <PostalCodeId>317702</PostalCodeId>
      </NotNeeded>
    </PostalCode>
  </CityRow>
  <CityRow CityId="53">
    <CityName>Abbotsford</CityName>
    <PostalCode PostalCodeName="V3G2J3">
      <NotNeeded>
        <PostalCodeId>317703</PostalCodeId>
      </NotNeeded>
    </PostalCode>
  </CityRow>
</Data>

Binding XML to schema

For the XML data to be shown in Word, it’s necessary that the xmlns attribute of the root tag points to the appropriate schema. To be precise – in our example, to show the XML data in the generated Word document, it’s not enough to provide just the following output from SQL:

SELECT Buyer, InvoiceDate, ...
FROM Invoice
FOR XML PATH('Invoice'), ELEMENTS

<Invoice>
    <Buyer>John Doe</Buyer>
    <InvoiceDate>2008-01-01</InvoiceDate>
    ...
</Invoice>

It’s needed to set the xmlns attribute in such a manner to point to the targetNamespace of the WordGeneratedInvoice.xsd schema:

WITH XMLNAMESPACES(DEFAULT 
   'http://schemas.microsoft.com/GeneratingWordDocuments/WordGeneratedInvoice.xsd')
SELECT Buyer, InvoiceDate, ...
FROM Invoice 
FOR XML PATH('Invoice'), ELEMENTS

<Data xmlns="http://schemas.microsoft.com/GeneratingWordDocuments/WordGeneratedInvoice.xsd">
    <Buyer>John Doe</Buyer>
    <InvoiceDate>2008-01-01</InvoiceDate>
    ...
</Invoice>

A blank Word document is the most common result if the XML data is not bound to the schema over an xmlns attribute.

Applying the transformation on XML data

public static byte[] GetWord(XmlReader xmlData, XmlReader xslt)
{
    XslCompiledTransform xslt = new XslCompiledTransform();
    XsltArgumentList args = new XsltArgumentList();

    using (MemoryStream swResult = new MemoryStream())
    {
        xslt.Load(xslt);
        xslt.Transform(xmlData, args, swResult);

        return swResult.ToArray();
    }
}

It’s mentioned earlier that this step is trivial. The example justifies that, doesn’t it?

After the XML data and the XSL transformation are passed as XmlReader objects, an XslCompiledTransform is initialized through the Load method. All that is left is to call Transform to finish the job.

XML->XSLT->HTML->Word, the easy way out

In case you don’t need advanced capabilities that Word provides (page numbering, margins, and similar), you have a pretty handy option of hand-writing XSLT that transforms XML data to HTML and then just opens HTML in Word.

To illustrate the idea with an example – here is an XSLT that I use for a list report that just shows the contents of a CD DataTable with two columns, Title and Price:

='1.0'='UTF-8'
<xsl:stylesheet xmlns:xsl='http://www.w3.org/1999/XSL/Transform' version='1.0' 
xmlns:fo='http://www.w3.org/1999/XSL/Format' 
xmlns:fn='http://www.w3.org/2003/11/xpath-functions' 
xmlns:xf='http://www.w3.org/2002/08/xquery-functions'>
    <xsl:template match='/'>
        <html>
            <body>
                <h2>Report Header</h2>
                <table border='0' width='100%'>
                    <tr bgcolor='Gray'>
                        <th align='left'>Title</th>
                        <th align='left'>Price</th>
                    </tr>
                    <xsl:for-each select='DocumentElement/Cd'>
                        <tr>
                            <td>
                                <xsl:value-of select='Title'/>
                            </td>
                            <td>
                                <xsl:value-of select='Price'/>
                            </td>
                        </tr>
                    </xsl:for-each>
                </table>
            </body>
        </html>
    </xsl:template>
</xsl:stylesheet>

The XML data which is transformed:

='1.0'='UTF-8'
<DocumentElement>
    <Cd>
        <Title>Mike</Title>
        <Price>20$</Price>
    </Cd>
    <Cd>
        <Title>Nike</Title>
        <Price>30$</Price>
    </Cd>
    <Cd>
        <Title>Reebok</Title>
        <Price>40$</Price>
    </Cd>
</DocumentElement>

When the xsl:template tag is matched (and it’ll be matched always because it points to root), its InnerText is evaluated. The xsl:for-each tag processes each of the DocumentElement/Cd nodes, and xsl:value-of gets the InnerText of the XPath selected element. In case you’re not too good with XSLT, I recommend this webpage: W3Schools. W3Schools, you rock! :)

Resulting HTML:

<html xmlns:fo="http://www.w3.org/1999/XSL/Format" 
xmlns:fn="http://www.w3.org/2003/11/xpath-functions" 
xmlns:xf="http://www.w3.org/2002/08/xquery-functions">
    <body>
        <h2>Something</h2>
        <table border="0" width="100%">
            <tr bgcolor="Gray">
                <th align="left">Title</th>
                <th align="left">Price</th>
            </tr>
            <tr>
                <td>Mike</td>
                <td>20$</td>
            </tr>
            <tr>
                <td>Nike</td>
                <td>30$</td>
            </tr>
            <tr>
                <td>Reebok</td>
                <td>40$</td>
            </tr>
        </table>
    </body>
</html>

Word, even in versions earlier than 2003, had no any problems with opening HTML; so, just save the result as .doc (instead of .HTML) and you’ll be done. In case you are sending the response over the Web, you can specify the type with:

Response.AddHeader("content-type", "application/msword");
Response.AddHeader("Content-Disposition", "attachment; filename=report.doc");

The true value of this option comes into light when you start thinking about generic reports. In the source code that accompanies this article, you’ll find a generic version of this example, the one that works with any DataTable. Be sure to check it.

Organization of resources used for the generation in the Visual Studio project

The source code I have attached to this article demonstrates one possible way of organizing the needed resources for the Word reports generation. Here is the project structure:

Figure 11 - XSL transform as part of VS.NET project for generating Word reports

Figure 11 — XSL transform as part of the VS.NET project for generating Word reports

It is of utmost importance that Embedded Resource is set on the Build Action for all the resources that are used in the generation of the Word document (XML, XSD, XSLT). This enables their later fetching from the resource collection of the compiled DLL.

Reports are generated through a static Report class which represents the facade to embedded resources and the logic exploiting them:

public class Report
{
                    public static byte[] WordGeneratedInvoice()
    {
        

        

        

        string xmlData = Getters.GetTestXml("WordGeneratedInvoice");
        return Getters.GetWord(xmlData, "WordGeneratedInvoice");
    }

    


    

}

Adding new reports in this structure is easy:

  • The new report for generation is added in the Doc directory.
  • The XML schema which is created based on the report is added in the Xsd directory.
  • After the schema is applied on the document, the saved WordML is used as the input in the WML2XSLT tool; the resulting XSLT is placed in the Xslt directory.
  • A method is added in the Report class which is responsible for fetching XML data, invoking the transformation, and returning the resulting Word document.

FAQ

Can I convert the generated WordML to PDF? How do I do it?

Check out my article Generate PDF using C#.

I applied the schema to the Word document and ended my work on it. After some time, I reopened the document, but in the XML structure dialog, the list of elements available for applying onto the document (the lower listbox) is empty.

This occurred because the path to the XSD file is changed. The location of the schema can be refreshed by using XML Options -> Schema Library -> choose the schema used in the document -> Schema Settings -> Browse…

Figure 12 – Dialogs (from left to right) that visualy show path to Browse... option

Figure 12 – Dialogs (ordered from left to right) that visually show the path to the Browse… option

I’ve changed the XML schema (XSD) after the changed request for the new report fields arrived. However, Word 2003 does not show new fields in the XML structure dialog, so I can’t bind them to the data in the new version of the report. Must I build the report from scratch?

This problem can be solved by installing the Office 2003 Service Pack 2. When SP2 is installed, Word 2003 will refresh the attached schema if the following steps are satisfied:

  • Schema is changed, XSD file is saved
  • All instances of Word 2003 are closed (not only the document which uses the mentioned schema!)
  • Reopen the Word document that uses the schema

In some situations, the better way to solve this problem is to install the XML Toolbox for Microsoft Office Word 2003 – it adds the command Refresh Schema. The solution isn’t universal because the XML Toolbox doesn’t install properly always (the most common problems are security polices, the existence of .NET Framework 1.1…). So, my suggestion is to close all Office applications, download the .msi from the link, run it – if everything goes smoothly, you’ll see the Word XML toolbar (View -> Toolbars -> Word XML Toolbar); if not, you always have the first suggestion for schema refreshing.

Figure 13 - XML Toolbox in Word 2003, with Reload Schema option

Figure 13 — XML Toolbox in Word 2003, with the Reload Schema option

I made the XSD, bound it to the Word document, made the XSLT, prepared the XML data, performed the transformation, and got – empty document

The most common cause of this problem is that XML doesn’t contain the schema binding (as a value of the xmlns attribute of the root tag). Read the Binding XML to schema chapter.

The easiest way to see the type of XML you should prepare is to get the properly schema and fields bounded Word document to be saved on the some temporary location as the XML (File -> Save as, Save as type: XML document), by checking the option Save data only. You can view the saved XML by opening it in Visual Studio .NET or Notepad…

Figure 14 – Saving XML data only from properly mapped Word document

Figure 14 – Saving the XML data only from properly mapped Word document

Where are the XML options located in Office Word 2007? How different is report making between Word 2003 and 2007?

Honestly, I haven’t worked much in the 2007 version of Word, but still — I couldn’t find big differences. The only problem I’ve had is in finding the XML Structure dialog, as it was not accessible with the Task Pane. It seems that the XML Toolbox is installed by default with Office 2007, so you can solve this by its adding by using the toolbar’s (Ribbon’s) option, Customize…

Figure 15 – Dialog shown after choosing option Customize... in Word 2007

Figure 15 – Dialog shown after choosing the option Customize… in Word 2007

Figure 16 - XML Toolbox in Word 2007

Figure 16 — XML Toolbox in Word 2007

Conclusion

It is worthy to note that the solution I recommended doesn’t use Visual Studio Tools for Office. I tried them out for document generation, and was very disappointed as they required a nasty deal of configuring both to develop and run.

Also, using XSLT to produce Word documents is far easier than juggling with the Microsoft Word Object Library COM DLL and its Word.Application class; not to mention that it is way faster and memory leak free. If you are using a COM DLL for generating Word files, I would advise you to start rewriting that part of your system right now, especially if you are generating documents on the server and then sending them to clients. Simply, Word was developed to be an interactive user application and not a «visible = false» puppet of another process.

Well, that’s it folks. You know the drill — please take your time to rate this article, and if you are (un)happy with it or just need some aid, post comments and I’ll be glad to respond/help in no time :).

References

In no particular order…

Books:

History

  • February 17, 2008 — Added the multiple images example (this comment initiated it).
  • November 4, 2007 — Added the image example (this comment initiated it).
  • October 17, 2007 – Added the grouping example (this comment initiated it).
  • September 13, 2007 – Added the read-only section, added one more example (this comment initiated it).
  • August 31, 2007 – Initial version of the article.

If you liked this article, consider reading other articles by me. For republishing article on other websites, please contact me by leaving a comment.

In today’s web applications, quite often you run into the need to create a Word document that user can download. The two most common ways of accomplish this is to install a copy of Word with Office Interop on the server or use a third-party library. Both of these options have significant downsides; however, there is a far simpler way to create Word Documents using XML. Because Word natively has the ability to read/write Word documents in XML, you can utilize all of the XML/XSLT tools in .NET to create them.

The first step is to create a normal Word document to be used as a template. Figure 1 shows a simple Word doc with which you will start to produce a report for Sales Territories from the Adventure Works sample database.

Figure 1: Word Document Template

Although this document does not make much sense at this point, you will use the markers in the document to help build the XSLT. To save this document to an XML file, you will need to save the file in the Word 2003 XML Document format shown in Figure 2.

Figure 2: Save as Word XML

Creating the XSLT

Now that you have an XML file, you can start the process of creating an XSLT (EXtensible Stylesheet Language Transformation). First, change the extension of the file from XML to XSLT. Then, open the XSLT in an XML editing tool. Next, you need to add a couple of tags at the top and bottom so that you can call this a XSLT. At this point, the top of the XSLT document should show the following couple of tags:

<?xml version="1.0" encoding="utf-8"?>
<w:wordDocument xmlns_aml=
   "http://schemas.microsoft.com/aml/2001/core" ...

You need to add two lines between these, as shown below:

<xsl:stylesheet version="1.0"
   xmlns_xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">

Then, at the end of the file, you need to add the following two lines to close out those tags you added to the top:

</xsl:template>
</xsl:stylesheet>

Now, you have a very basic XSLT; however, you have not really done anything interesting with it yet. Before you go further into the XSLT, you need to take a look at the XML you are going to create to build your Word doc.

<?xml version="1.0" encoding="utf-8"?>
<doc>
   <title>The Title of my Document</title>
   <region>
      <name>Region #1</name>
      <salesdiff>$123,000</salesdiff>
      <group>
         <group>
            <groupname>Group #1</groupname>
            <salesdiff>$300</salesdiff>
         </group>
      </group>
      <group>
         <group>
            <groupname>Group #2</groupname>
            <salesdiff>$300</salesdiff>
        </group>
      </group>
   </region>
   <region>
      <group>
         <group>
            <groupname>Group #1</groupname>
            <salesdiff>$300</salesdiff>
         </group>
      </group>
      <group>
         <groupname>Group #2</groupname>
         <salesdiff>$300</salesdiff>
      </group>
   </region>
</doc>

This XML file contains two regions; within each region are two groups. Now that you have a sample XML file, you have a reference to use in adding to the XSLT. Starting with the document title, search the XSLT for the text “Document Title”. Then, replace “Document Title” with <xsl:value-of select=”doc/title”/>. After modifying the line in the XML, it should look the same as the following:

<w:t><xsl:value-of select="doc/title"/></w:t>

This modification inserts the Title into this position in the document. Next, you’ll dig into the region and groups. A couple of lines down in the XSLT, after the line above, you should locate a line as follows:

</w:p>

After this line, add the following XSLT statement to loop through part of the document for each region.

<xsl:for-each select="doc/region">

Again, you will need to close the XSLT statement towards the end of the document. The closing statement should appear after the </w:tbl>. Now that you are at this point, it would be good to explain a couple of the tags in the XML document. If you are familiar with HTML table tags, there are some similarities, such as <w:tbl> for table, <w:tr> for table row, and <w:tc> for table cell. Next, you need to put in the tags to populate the region values within the loop. Search for the text “Region”. Replace this line with the line below:

<w:t><xsl:value-of select="name"/> --
     <xsl:value-of select="salesdiff"/></w:t>

This line places the name and salesdiff values for this region. The group below region is performed using the same process of the <xsl:for-each> and <xsl:value-of> statements.

Transformation Method

Next, build the code to create the XML data and transform it using the XSLT you just created. Listed below is the method that uses all of this and creates the Word document.

private void TransformToDoc(XmlDocument doc)
{
   // XslTransform class for loading an XSLT file.
   XslCompiledTransform _transform;

   // String: Path to XSLT file.
   string _xslPath;

   // Get paths for XSLT file
   _xslPath = Server.MapPath("Template.xslt");

   string filename = "Document - " +
      DateTime.Now.ToString("MM-dd-yyyy HHmmss") + ".doc";
   Page.Response.Clear();
   Page.Response.Buffer = true;
   Page.Response.AddHeader("content-disposition",
      "attachment;filename=" + filename);
   Page.Response.ContentType = "application/x-msword";
      Page.Response.Charset = null;

   // Transform _transform = new XslCompiledTransform();
   _transform.Load(_xslPath);
   _transform.Transform(doc, null, Page.Response.OutputStream);
   Page.Response.Flush();
   Page.Response.End();
}

By using the input XMLDocument, this method transforms the XML into Word compatible XML using the XSLT you created above. This method also populates the HTTP header and Content Type information so the web browser will prompt the user to open the document with Microsoft Word. In the event that you want to save the XML document directly, it is fairly simple to remove the Page statements and change the _transform.Transform call to output to a file instead of the OutputStream.

Conclusion

At this point, you have created a very simple process to convert a Word document to an XML file, then to an XSLT file that you can use to convert XML data to a format readable by Word. This process is compatible with both Word 2003 and 2007 and works especially well with web applications; with the proper header values, Word will automatically open the file transparently to the user. Its important to note that, because this approach uses XML, it can produce files that can become quite large. However, after creating the XSLT, this is quite simple and works very well for reports without the need for third-party libraries.

Download the Code

You can download the code that accompanies the article here.

About the Author

Chris Bennett is a manager with Crowe Horwath LLP in the Indianapolis office. He can be reached at chris.bennett@crowehorwath.com.

Задача обработки документов в формате docx, а также таблиц xlsx и презентаций pptx является весьма нетривиальной. В этой статье расскажу как научиться парсить, создавать и обрабатывать такие документы используя только XSLT и ZIP архиватор.

Зачем?

docx — самый популярный формат документов, поэтому задача отдавать информацию пользователю в этом формате всегда может возникнуть. Один из вариантов решения этой проблемы — использование готовой библиотеки, может не подходить по ряду причин:

  • библиотеки может просто не существовать
  • в проекте не нужен ещё один чёрный ящик
  • ограничения библиотеки по платформам и т.п.
  • проблемы с лицензированием
  • скорость работы

Поэтому в этой статье будем использовать только самые базовые инструменты для работы с docx документом.

Структура docx

Для начала разоберёмся с тем, что собой представляет docx документ. docx это zip архив который физически содержит 2 типа файлов:

  • xml файлы с расширениями xml и rels
  • медиа файлы (изображения и т.п.)

А логически — 3 вида элементов:

  • Типы (Content Types) — список типов медиа файлов (например png) встречающихся в документе и типов частей документов (например документ, верхний колонтитул).
  • Части (Parts) — отдельные части документа, для нашего документа это document.xml, сюда входят как xml документы так и медиа файлы.
  • Связи (Relationships) идентифицируют части документа для ссылок (например связь между разделом документа и колонтитулом), а также тут определены внешние части (например гиперссылки).

Они подробно описаны в стандарте ECMA-376: Office Open XML File Formats, основная часть которого — PDF документ на 5000 страниц, и ещё 2000 страниц бонусного контента.

Минимальный docx

Простейший docx после распаковки выглядит следующим образом

image

Давайте посмотрим из чего он состоит.

[Content_Types].xml

Находится в корне документа и перечисляет MIME типы содержимого документа:

<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
    <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
    <Default Extension="xml" ContentType="application/xml"/>
    <Override PartName="/word/document.xml"
              ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>

_rels/.rels

Главный список связей документа. В данном случае определена всего одна связь — сопоставление с идентификатором rId1 и файлом word/document.xml — основным телом документа.

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
    <Relationship 
        Id="rId1" 
        Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
        Target="word/document.xml"/>
</Relationships>

word/document.xml

Основное содержимое документа.

word/document.xml

<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:o="urn:schemas-microsoft-com:office:office"
            xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
            xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
            xmlns:v="urn:schemas-microsoft-com:vml"
            xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing"
            xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
            xmlns:w10="urn:schemas-microsoft-com:office:word"
            xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
            xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
            xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup"
            xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk"
            xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml"
            xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"
            mc:Ignorable="w14 wp14">
    <w:body>
        <w:p w:rsidR="005F670F" w:rsidRDefault="005F79F5">
            <w:r>
                <w:t>Test</w:t>
            </w:r>
            <w:bookmarkStart w:id="0" w:name="_GoBack"/>
            <w:bookmarkEnd w:id="0"/>
        </w:p>
        <w:sectPr w:rsidR="005F670F">
            <w:pgSz w:w="12240" w:h="15840"/>
            <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" 
                     w:header="720" w:footer="720" w:gutter="0"/>
            <w:cols w:space="720"/>
            <w:docGrid w:linePitch="360"/>
        </w:sectPr>
    </w:body>
</w:document>

Здесь:

  • <w:document> — сам документ
  • <w:body> — тело документа
  • <w:p> — параграф
  • <w:r> — run (фрагмент) текста
  • <w:t> — сам текст
  • <w:sectPr> — описание страницы

Если открыть этот документ в текстовом редакторе, то увидим документ из одного слова Test.

word/_rels/document.xml.rels

Здесь содержится список связей части word/document.xml. Название файла связей создаётся из названия части документа к которой он относится и добавления к нему расширения rels. Папка с файлом связей называется _rels и находится на том же уровне, что и часть к которой он относится. Так как связей в word/document.xml никаких нет то и в файле пусто:

<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
</Relationships>

Даже если связей нет, этот файл должен существовать.

docx и Microsoft Word

docx созданный с помощью Microsoft Word, да в принципе и с помощью любого другого редактора имеет несколько дополнительных файлов.

image

Вот что в них содержится:

  • docProps/core.xml — основные метаданные документа согласно Open Packaging Conventions и Dublin Core [1], [2].
  • docProps/app.xml — общая информация о документе: количество страниц, слов, символов, название приложения в котором был создан документ и т.п.
  • word/settings.xml — настройки относящиеся к текущему документу.
  • word/styles.xml — стили применимые к документу. Отделяют данные от представления.
  • word/webSettings.xml — настройки отображения HTML частей документа и настройки того, как конвертировать документ в HTML.
  • word/fontTable.xml — список шрифтов используемых в документе.
  • word/theme1.xml — тема (состоит из цветовой схемы, шрифтов и форматирования).

В сложных документах частей может быть гораздо больше.

Реверс-инжиниринг docx

Итак, первоначальная задача — узнать как какой-либо фрагмент документа хранится в xml, чтобы потом создавать (или парсить) подобные документы самостоятельно. Для этого нам понадобятся:

  • Архиватор zip
  • Библиотека для форматирования XML (Word выдаёт XML без отступов, одной строкой)
  • Средство для просмотра diff между файлами, я буду использовать git и TortoiseGit

Инструменты

  • Под Windows: zip, unzip, libxml2, git, TortoiseGit
  • Под Linux: apt-get install zip unzip libxml2 libxml2-utils git

Также понадобятся скрипты для автоматического (раз)архивирования и форматирования XML.
Использование под Windows:

  • unpack file dir — распаковывает документ file в папку dir и форматирует xml
  • pack dir file — запаковывает папку dir в документ file

Использование под Linux аналогично, только ./unpack.sh вместо unpack, а pack становится ./pack.

Использование

Поиск изменений происходит следующим образом:

  1. Создаём пустой docx файл в редакторе.
  2. Распаковываем его с помощью unpack в новую папку.
  3. Коммитим новую папку.
  4. Добавляем в файл из п. 1. изучаемый элемент (гиперссылку, таблицу и т.д.).
  5. Распаковываем изменённый файл в уже существующую папку.
  6. Изучаем diff, убирая ненужные изменения (перестановки связей, порядок пространств имён и т.п.).
  7. Запаковываем папку и проверяем что получившийся файл открывается.
  8. Коммитим изменённую папку.

Пример 1. Выделение текста жирным

Посмотрим на практике, как найти тег который определяет форматирование текста жирным шрифтом.

  1. Создаём документ bold.docx с обычным (не жирным) текстом Test.
  2. Распаковываем его: unpack bold.docx bold.
  3. Коммитим результат.
  4. Выделяем текст Test жирным.
  5. Распаковываем unpack bold.docx bold.
  6. Изначально diff выглядел следующим образом:

diff
Рассмотрим его подробно:

docProps/app.xml

@@ -1,9 +1,9 @@
-  <TotalTime>0</TotalTime>
+  <TotalTime>1</TotalTime>

Изменение времени нам не нужно.

docProps/core.xml

@@ -4,9 +4,9 @@
-  <cp:revision>1</cp:revision>
+  <cp:revision>2</cp:revision>
   <dcterms:created xsi:type="dcterms:W3CDTF">2017-02-07T19:37:00Z</dcterms:created>
-  <dcterms:modified xsi:type="dcterms:W3CDTF">2017-02-07T19:37:00Z</dcterms:modified>
+  <dcterms:modified xsi:type="dcterms:W3CDTF">2017-02-08T10:01:00Z</dcterms:modified>

Изменение версии документа и даты модификации нас также не интересует.

word/document.xml

diff

@@ -1,24 +1,26 @@
    <w:body>
-    <w:p w:rsidR="0076695C" w:rsidRPr="00290C70" w:rsidRDefault="00290C70">
+    <w:p w:rsidR="0076695C" w:rsidRPr="00F752CF" w:rsidRDefault="00290C70">
       <w:pPr>
         <w:rPr>
+          <w:b/>
           <w:lang w:val="en-US"/>
         </w:rPr>
       </w:pPr>
-      <w:r>
+      <w:r w:rsidRPr="00F752CF">
         <w:rPr>
+          <w:b/>
           <w:lang w:val="en-US"/>
         </w:rPr>
         <w:t>Test</w:t>
       </w:r>
       <w:bookmarkStart w:id="0" w:name="_GoBack"/>
       <w:bookmarkEnd w:id="0"/>
     </w:p>
-    <w:sectPr w:rsidR="0076695C" w:rsidRPr="00290C70">
+    <w:sectPr w:rsidR="0076695C" w:rsidRPr="00F752CF">

Изменения в w:rsidR не интересны — это внутренняя информация для Microsoft Word. Ключевое изменение тут

         <w:rPr>
+          <w:b/>

в параграфе с Test. Видимо элемент <w:b/> и делает текст жирным. Оставляем это изменение и отменяем остальные.

word/settings.xml

@@ -1,8 +1,9 @@
+  <w:proofState w:spelling="clean"/>
@@ -17,10 +18,11 @@
+    <w:rsid w:val="00F752CF"/>

Также не содержит ничего относящегося к жирному тексту. Отменяем.

7 Запаковываем папку с 1м изменением (добавлением <w:b/>) и проверяем что документ открывается и показывает то, что ожидалось.
8 Коммитим изменение.

Пример 2. Нижний колонтитул

Теперь разберём пример посложнее — добавление нижнего колонтитула.
Вот первоначальный коммит. Добавляем нижний колонтитул с текстом 123 и распаковываем документ. Такой diff получается первоначально:

diff

Сразу же исключаем изменения в docProps/app.xml и docProps/core.xml — там тоже самое, что и в первом примере.

[Content_Types].xml

@@ -4,10 +4,13 @@
   <Default Extension="xml" ContentType="application/xml"/>
   <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
+  <Override PartName="/word/footnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"/>
+  <Override PartName="/word/endnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"/>
+  <Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>

footer явно выглядит как то, что нам нужно, но что делать с footnotes и endnotes? Являются ли они обязательными при добавлении нижнего колонтитула или их создали заодно? Ответить на этот вопрос не всегда просто, вот основные пути:

  • Посмотреть, связаны ли изменения друг с другом
  • Экспериментировать
  • Ну а если совсем не понятно что происходит:

Читать документацию
Идём пока что дальше.

word/_rels/document.xml.rels

Изначально diff выглядит вот так:

diff

@@ -1,8 +1,11 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
   <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
+  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
   <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
   <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
-  <Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
-  <Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
+  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
+  <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" Target="endnotes.xml"/>
+  <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/>
 </Relationships>

Видно, что часть изменений связана с тем, что Word изменил порядок связей, уберём их:

@@ -3,6 +3,9 @@
+  <Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
+  <Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" Target="endnotes.xml"/>
+  <Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/>

Опять появляются footer, footnotes, endnotes. Все они связаны с основным документом, перейдём к нему:

word/document.xml

@@ -15,10 +15,11 @@
       </w:r>
       <w:bookmarkStart w:id="0" w:name="_GoBack"/>
       <w:bookmarkEnd w:id="0"/>
     </w:p>
     <w:sectPr w:rsidR="0076695C" w:rsidRPr="00290C70">
+      <w:footerReference w:type="default" r:id="rId6"/>
       <w:pgSz w:w="11906" w:h="16838"/>
       <w:pgMar w:top="1134" w:right="850" w:bottom="1134" w:left="1701" w:header="708" w:footer="708" w:gutter="0"/>
       <w:cols w:space="708"/>
       <w:docGrid w:linePitch="360"/>
     </w:sectPr>

Редкий случай когда есть только нужные изменения. Видна явная ссылка на footer из sectPr. А так как ссылок в документе на footnotes и endnotes нет, то можно предположить что они нам не понадобятся.

word/settings.xml

diff

@@ -1,19 +1,30 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <w:settings xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" mc:Ignorable="w14 w15">
   <w:zoom w:percent="100"/>
+  <w:proofState w:spelling="clean"/>
   <w:defaultTabStop w:val="708"/>
   <w:characterSpacingControl w:val="doNotCompress"/>
+  <w:footnotePr>
+    <w:footnote w:id="-1"/>
+    <w:footnote w:id="0"/>
+  </w:footnotePr>
+  <w:endnotePr>
+    <w:endnote w:id="-1"/>
+    <w:endnote w:id="0"/>
+  </w:endnotePr>
   <w:compat>
     <w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/>
     <w:compatSetting w:name="overrideTableStyleFontSizeAndJustification" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="enableOpenTypeFeatures" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="doNotFlipMirrorIndents" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
     <w:compatSetting w:name="differentiateMultirowTableHeaders" w:uri="http://schemas.microsoft.com/office/word" w:val="1"/>
   </w:compat>
   <w:rsids>
     <w:rsidRoot w:val="00290C70"/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:rsid w:val="001B0DE6"/>

А вот и появились ссылки на footnotes, endnotes добавляющие их в документ.

word/styles.xml

diff

@@ -480,6 +480,50 @@
       <w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
       <w:b/>
       <w:sz w:val="28"/>
     </w:rPr>
   </w:style>
+  <w:style w:type="paragraph" w:styleId="a4">
+    <w:name w:val="header"/>
+    <w:basedOn w:val="a"/>
+    <w:link w:val="a5"/>
+    <w:uiPriority w:val="99"/>
+    <w:unhideWhenUsed/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:pPr>
+      <w:tabs>
+        <w:tab w:val="center" w:pos="4677"/>
+        <w:tab w:val="right" w:pos="9355"/>
+      </w:tabs>
+      <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
+    </w:pPr>
+  </w:style>
+  <w:style w:type="character" w:customStyle="1" w:styleId="a5">
+    <w:name w:val="Верхний колонтитул Знак"/>
+    <w:basedOn w:val="a0"/>
+    <w:link w:val="a4"/>
+    <w:uiPriority w:val="99"/>
+    <w:rsid w:val="000A7B7B"/>
+  </w:style>
+  <w:style w:type="paragraph" w:styleId="a6">
+    <w:name w:val="footer"/>
+    <w:basedOn w:val="a"/>
+    <w:link w:val="a7"/>
+    <w:uiPriority w:val="99"/>
+    <w:unhideWhenUsed/>
+    <w:rsid w:val="000A7B7B"/>
+    <w:pPr>
+      <w:tabs>
+        <w:tab w:val="center" w:pos="4677"/>
+        <w:tab w:val="right" w:pos="9355"/>
+      </w:tabs>
+      <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
+    </w:pPr>
+  </w:style>
+  <w:style w:type="character" w:customStyle="1" w:styleId="a7">
+    <w:name w:val="Нижний колонтитул Знак"/>
+    <w:basedOn w:val="a0"/>
+    <w:link w:val="a6"/>
+    <w:uiPriority w:val="99"/>
+    <w:rsid w:val="000A7B7B"/>
+  </w:style>
 </w:styles>

Изменения в стилях нас интересуют только если мы ищем как поменять стиль. В данном случае это изменение можно убрать.

Посмотрим теперь собственно на сам нижний колонтитул (часть пространств имён опущена для читабельности, но в документе они должны быть):

<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
  <w:p w:rsidR="000A7B7B" w:rsidRDefault="000A7B7B">
    <w:pPr>
      <w:pStyle w:val="a6"/>
    </w:pPr>
    <w:r>
      <w:t>123</w:t>
    </w:r>
  </w:p>
</w:ftr>

Тут виден текст 123. Единственное, что надо исправить — убрать ссылку на <w:pStyle w:val="a6"/>.

В результате анализа всех изменений делаем следующие предположения:

  • footnotes и endnotes не нужны
  • В [Content_Types].xml надо добавить footer
  • В word/_rels/document.xml.rels надо добавить ссылку на footer
  • В word/document.xml в тег <w:sectPr> надо добавить <w:footerReference>

Уменьшаем diff до этого набора изменений:

final diff

Затем запаковываем документ и открываем его.
Если всё сделано правильно, то документ откроется и в нём будет нижний колонтитул с текстом 123. А вот и итоговый коммит.

Таким образом процесс поиска изменений сводится к поиску минимального набора изменений, достаточного для достижения заданного результата.

Практика

Найдя интересующее нас изменение, логично перейти к следующему этапу, это может быть что-либо из:

  • Создания docx
  • Парсинг docx
  • Преобразования docx

Тут нам потребуются знания XSLT и XPath.

Давайте напишем достаточно простое преобразование — замену или добавление нижнего колонтитула в существующий документ. Писать я буду на языке Caché ObjectScript, но даже если вы не знаете — не беда. В основном будем вызовать XSLT и архиватор. Ничего более. Итак, приступим.

Алгоритм

Алгоритм выглядит следующим образом:

  1. Распаковываем документ
  2. Добавляем наш нижний колонтитул
  3. Прописываем ссылку на него в [Content_Types].xml и word/_rels/document.xml.rels
  4. В word/document.xml в тег <w:sectPr> добавляем тег <w:footerReference> или заменяем в нём ссылку на наш нижний колонтитул.
  5. Запаковываем документ

Приступим.

Распаковка

В Caché ObjectScript есть возможность выполнять команды ОС с помощью функции $zf(-1, oscommand). Вызовем unzip для распаковки документа с помощью обёртки над $zf(-1):

/// Используя %3 (unzip) распаковать файл %1 в папку %2
Parameter UNZIP = "%3 %1 -d %2";

/// Распаковать архив source в папку targetDir
ClassMethod executeUnzip(source, targetDir) As %Status
{
    set timeout = 100
    set cmd = $$$FormatText(..#UNZIP, source, targetDir, ..getUnzip())
    return ..execute(cmd, timeout)
}

Создаём файл нижнего колонтитула

На вход поступает текст нижнего колонтитула, запишем его в файл in.xml:

<xml>TEST</xml>

В XSLT (файл — footer.xsl) будем создавать нижний колонтитул с текстом из тега xml (часть пространств имён опущена, вот полный список):

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  xmlns="http://schemas.openxmlformats.org/package/2006/relationships" version="1.0">
    <xsl:output method="xml" omit-xml-declaration="no" indent="yes" standalone="yes"/>
    <xsl:template match="/">

        <w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
            <w:p>
                <w:r>
                    <w:rPr>
                        <w:lang w:val="en-US"/>
                    </w:rPr>
                    <w:t>
                        <xsl:value-of select="//xml/text()"/>
                    </w:t>
                </w:r>
            </w:p>
        </w:ftr>
    </xsl:template>
</xsl:stylesheet>

Теперь вызовем XSLT преобразователь:

do ##class(%XML.XSLT.Transformer).TransformFile("in.xml", "footer.xsl", footer0.xml")    

В результате получится файл нижнего колонтитула footer0.xml:

<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
    <w:p>
        <w:r>
            <w:rPr>
                <w:lang w:val="en-US"/>
            </w:rPr>
            <w:t>TEST</w:t>
        </w:r>
    </w:p>
</w:ftr>

Добавляем ссылку на колонтитул в список связей основного документа

Сссылки с идентификатором rId0 как правило не существует. Впрочем можно использовать XPath для получения идентификатора которого точно не существует.
Добавляем ссылку на footer0.xml c идентификатором rId0 в word/_rels/document.xml.rels:

XSLT

<xsl:stylesheet  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns="http://schemas.openxmlformats.org/package/2006/relationships"  version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="no"  />
    <xsl:param name="new">
        <Relationship 
           Id="rId0" 
           Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" 
           Target="footer0.xml"/>
    </xsl:param>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:copy-of select="$new"/>
            <xsl:copy-of select="@* | node()"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Прописываем ссылки в документе

Далее надо в каждый тег <w:sectPr> добавить тег <w:footerReference> или заменить в нём ссылку на наш нижний колонтитул. Оказалось, что у каждого тега <w:sectPr> может быть 3 тега <w:footerReference> — для первой страницы, четных страниц и всего остального:

XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="yes" />
    <xsl:template match="//@* | //node()">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="//w:sectPr">
        <xsl:element name="{name()}" namespace="{namespace-uri()}">
            <xsl:copy-of select="./namespace::*"/>
            <xsl:apply-templates select="@*"/>
            <xsl:copy-of select="./*[local-name() != 'footerReference']"/>
            <w:footerReference w:type="default" r:id="rId0"/>
            <w:footerReference w:type="first" r:id="rId0"/>
            <w:footerReference w:type="even" r:id="rId0"/>
        </xsl:element>
    </xsl:template>
</xsl:stylesheet>

Добавляем колонтитул в [Content_Types].xml

Добавляем в [Content_Types].xml информацию о том, что /word/footer0.xml имеет тип application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml:

XSLT

<xsl:stylesheet  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  xmlns="http://schemas.openxmlformats.org/package/2006/content-types"  version="1.0">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="no"  />
    <xsl:param name="new">
        <Override 
         PartName="/word/footer0.xml" 
         ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
    </xsl:param>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:copy-of select="@* | node()"/> 
            <xsl:copy-of select="$new"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

В результате

Весь код опубликован. Работает он так:

do ##class(Converter.Footer).modifyFooter("in.docx", "out.docx", "TEST")

Где:

  • in.docx — исходный документ
  • out.docx — выходящий документ
  • TEST — текст, который добавляется в нижний колонтитул

Выводы

Используя только XSLT и ZIP можно успешно работать с документами docx, таблицами xlsx и презентациями pptx.

Открытые вопросы

  1. Изначально хотел использовать 7z вместо zip/unzip т… к. это одна утилита и она более распространена на Windows. Однако я столкнулся с такой проблемой, что документы запакованные 7z под Linux не открываются в Microsoft Office. Я попробовал достаточно много вариантов вызова, однако положительного результата добиться не удалось.
  2. Ищу XSD со схемами ECMA-376 версии 5 и комментариями. XSD версии 5 без комментариев доступен к загрузке на сайте ECMA, но без комментариев в нём сложно разобраться. XSD версии 2 с комментариями доступен к загрузке.

Ссылки

  • ECMA-376
  • Описание docx
  • Подробная статья про docx

Автор: InterSystems

Источник

This simple program demostrate how to create well formatted MS Word documents using C#, XML and XSLT. Using XSLT to create Word document requires the knowledge of RTF key words. RTF specification is available in MSDN site.

1. Open Visual Studio .NET and select File —> New —> Project.

2. Select Visual C# project as the Project Type and Windows Application as the Template. For the project name, specify ‘CShartXSLT’; for the path specify the location where you want the project to be created. Click OK to create the new project.

3. Add an XML file named «Employee.xml» in DebugBin directory (other wise you have to specify the path in the program). The structure of the file will be like,

<?xml version="1.0" encoding="utf-8" ?> 
<Employee> 
  <Record> 
    <EmpID>E1</EmpID> 
    <EmpName>Sudipta</EmpName> 
    <EmpAge>29</EmpAge> 
    <EmpSex>M</EmpSex> 
    <EmpAddress>Kolkata</EmpAddress> 
    <Department> 
      <DeptID>D1</DeptID> 
      <EmpID>E1</EmpID> 
      <DeptName>Sales</DeptName> 
    </Department> 
  </Record> 
  <Record> 
    <EmpID>E2</EmpID> 
    <EmpName>Chiranjib</EmpName> 
    <EmpAge>26</EmpAge> 
    <EmpSex>M</EmpSex> 
    <EmpAddress>Kolkata</EmpAddress> 
    <Department> 
      <DeptID>D1</DeptID> 
      <EmpID>E2</EmpID> 
      <DeptName>Sales</DeptName> 
    </Department> 
  </Record> 
  <Record> 
    <EmpID>E3</EmpID> 
    <EmpName>Nilanjan</EmpName> 
    <EmpAge>29</EmpAge> 
    <EmpSex>M</EmpSex> 
    <EmpAddress>Kolkata</EmpAddress> 
    <Department> 
      <DeptID>D2</DeptID> 
      <EmpID>E3</EmpID> 
      <DeptName>Finance</DeptName> 
    </Department> 
  </Record> 
  <Record> 
    <EmpID>E4</EmpID> 
    <EmpName>Chayan</EmpName> 
    <EmpAge>30</EmpAge> 
    <EmpSex>M</EmpSex> 
    <EmpAddress>Kolkata</EmpAddress> 
    <Department> 
      <DeptID>D3</DeptID> 
      <EmpID>E4</EmpID> 
      <DeptName>Human Resource</DeptName> 
    </Department> 
  </Record> 
  <Record> 
    <EmpID>E5</EmpID> 
    <EmpName>Biplab</EmpName> 
    <EmpAge>31</EmpAge> 
    <EmpSex>M</EmpSex> 
    <EmpAddress>Kolkata</EmpAddress> 
    <Department> 
      <DeptID>D4</DeptID> 
      <EmpID>E5</EmpID> 
      <DeptName>Administration</DeptName> 
    </Department> 
  </Record> 
</Employee>

4. Add an XSLT file named «Employee.xslt» in DebugBin directory (other wise you have to specify the path in the program). The structure of the file will be like,

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:dt="urn:schemas-microsoft-com:datatypes" xmlns:user="urn:my-scripts"> 
  <xsl:output method="text" /> 
  <xsl:template match="Employee"> 
    <xsl:text>{rtf1fs22</xsl:text> 
    <!--Print the Header Row--> 
    <xsl:text>trowdcellx2500cellx3000cellx3800cellx6800cellx9144intblkeepnqlb Namecell Agecell Sexcell Addresscell Departmentcell</xsl:text> 
    <xsl:text>b0ql0intblrow</xsl:text> 
    <!--Print Employee Records--> 
    <xsl:apply-templates select="Record" /> 
    <xsl:text>}</xsl:text> 
  </xsl:template> 
  <xsl:template match="Record"> 
    <xsl:text>trowdcellx2500cellx3000cellx3800cellx6800cellx9144intblkeepnql </xsl:text> 
    <xsl:value-of select="EmpName" /> 
    <xsl:text>cell </xsl:text> 
    <xsl:value-of select="EmpAge" /> 
    <xsl:text>cell </xsl:text> 
    <xsl:if test="EmpSex='M'"> 
      <xsl:text>Male</xsl:text> 
      <xsl:text>cell </xsl:text> 
    </xsl:if> 
    <xsl:if test="EmpSex='F'"> 
      <xsl:text>Female</xsl:text> 
      <xsl:text>cell </xsl:text> 
    </xsl:if> 
    <xsl:value-of select="EmpAddress" /> 
    <xsl:text>cell </xsl:text> 
    <!--Print Employee Department--> 
    <xsl:apply-templates select="Department" /> 
    <xsl:text>cell</xsl:text> 
    <xsl:text>intblrow</xsl:text> 
  </xsl:template> 
  <xsl:template match="Department"> 
    <xsl:value-of select="DeptName" /> 
  </xsl:template> 
</xsl:stylesheet> 

5. In Form1.cs, add the following references

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;
using System.IO;

6. In Form1.cs, add a button and write the following code

private void button1_Click(object sender, EventArgs e)
{
    DataSet ds;
    XmlDataDocument xmlDoc;
    XslCompiledTransform xslTran;
    XmlElement root;
    XPathNavigator nav;
    XmlTextWriter writer;
    try
    {
        //Create the DataSet from the XML file
        ds = new DataSet();
        ds.ReadXml("Employee.xml");
        //Create the XML from the DataSet
        xmlDoc = new XmlDataDocument(ds);
        //Load the XSLT for Transformation
        xslTran = new XslCompiledTransform();
        xslTran.Load("Employee.xslt");
        //Determine the Root object in the XML
        root = xmlDoc.DocumentElement;
        //Create the XPath Navigator to navigate throuth the XML
        nav = root.CreateNavigator();
        //First delete the RTF, if already exist
        if (File.Exists("Employee.rtf"))
        {
            File.Delete("Employee.rtf");
        }
        //Create the RTF by Transforming the XML and XSLT
        writer = new XmlTextWriter("Employee.rtf", System.Text.Encoding.Default);
        xslTran.Transform(nav, writer);
        //Close the Writer after Transformation
        writer.Close();
        //Release all objects
        writer = null;
        nav = null;
        root = null;
        xmlDoc = null;
        ds = null;
        MessageBox.Show("Document created successfully.....");
    }
    catch (Exception ex)
    {
        writer = null;
        nav = null;
        root = null;
        xmlDoc = null;
        ds = null;
        MessageBox.Show(ex.StackTrace);
    }
}

7. Compile and run the program.

Using Xml, Xsd and XSLT Identity Transform to template and generate (Word) documents (part 2)

In part one we talked about using xslt to create simple xml templates and fill them with a identity transform using xslt. We can use this approach for every xml document thinkable, but for now I will focus on Word documents as those are fairly common in our line of work Glimlach. The first thing to ask ourselves is, how do we get our own custom xml placeholders in Word, maybe we want different kinds of placeholders, like ones were a value will be put or ones that denote a conditional block of Word content.

The answer is simple, Word allows us to create our own xsd and hook that into a Word document, now this opens up all kinds of possibilities for custom functionality in our Word templates. So first, lets create a xsd. All the files shown will be attached for everyone to download in a .zip file so if you want to have a better look don&rsquo;t worry. Our xsd looks like this:

image

This is a fairly simple xsd, ALWAYS provide a target namespace for your schema so your own elements can be identified as your own elements. This namespace will also be declared in our xsd with the prefix tns: so we can use this namespace to refer to our global types, global types and elements will belong to the targetNamespace by default.

Let&rsquo;s look at the elements and types. First I have an element that will be applied to the entire Word document, this will be our document node, it is called WordTemplate. The type is templateType which is defined below it. This type specifies a xs:choice which contains a valueHolder element. Now why would we use xs:choice and not for example sequence? The answer is because I don&rsquo;t want to add different kinds of placeholders in our Word document in a particular order. Now we have only one kind, but later on I may want to provide additional place holders. the only way to specify an unordered set of different kinds of optional elements in xsd is with a xs:choice which can occur multiple times. The valueHolder element will have one attribute named ‘query&rsquo;. In our Word template this will be filled with a xpath query which defines which property value from .Net will be placed here.

Now how do we couple this xsd in our own Word template. When you open Word 2010 you need to enable the developer tab. Go to File &ndash;> Options &ndash;> Customize Ribbon and check the developer tab. From the developer tab you will see an option for schema. With this option you can add your own schema. Afterwards you can click the structure button and your Word document will look like this:

image

To the right you can see our own Schema elements, to only option we have at first is to apply our document node. So lets do that. In order to do so, all you need to do is to click on our element, Inside our document node we can apply our valueholder elements. I will apply it on places where I want values from .Net inserted. Afterwards our document will look like this:

image

You can see all our elements. When you press ctrl shift X, Word will toggle between this view and the view without the tags. To the right you can see yellow signs before the valueHolder elements this means those tags are invalid according to our schema. Word is right! Recall that those elements need to have a required attribute called query. So we right click all those elements and fill in the attributes like this:

image

Here I right clicked the valueHolder element next to Name: and I filled in a xpath query. From our template we need to know the structure of the .Net object we are going to work with. Specifying this in the template allows us to later add extra properties of the object, without modifying code but with only replacing the template. I will fill in the rest and save the document. You can save the document as a standard Word 2010 file, it will be .docx which is actually a zip file with other files in it. This approach will still work but in .Net we would have to use the packaging API to get the content.xml file and then transform it using xslt. For simplicity I will save the file as a Word 2010 xml file so all the info is contained in one file, no need for extracting.

Now where is the Identity transform I talked about? Well, right below:

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <xsl:stylesheet version="1.0"
   3:     >="http://www.w3.org/1999/XSL/Transform"
   4:     >="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
   5:     >="urn:Chris:demo:word"
   6:     >="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
   7:&nbsp; 
   8:   <xsl:output method="xml" indent="yes"/>
   9:&nbsp; 
  10:   <xsl:template match="@* | node()">
  11:     <xsl:copy>
  12:       <xsl:apply-templates select="@* | node()"/>
  13:     </xsl:copy>
  14:   </xsl:template>
  15:&nbsp; 
  16:   <xsl:template
  17:      match="w:customXml[@w:element='valueHolder']/w:tc/w:p" >
  18:     <xsl:variable name="query" 
  19:     select="../../w:customXmlPr/w:attr[@w:name='query']/@w:val">
  20:     </xsl:variable>
  21:     <xsl:copy>
  22:       <xsl:apply-templates select="@* | node()"/>
  23:       <w:r>
  24:         <w:t>
  25:           <xsl:value-of 
  26:           select="wordgen:GetMessageValueByXpath($query)"/>
  27:         </w:t>
  28:       </w:r>
  29:     </xsl:copy>
  30:   </xsl:template>
  31: </xsl:stylesheet>

Above is a xslt stylesheet created in VS2010. What strikes is that the identity transform I used in part one was defined as match=&rsquo;attribute() | node()&rsquo;. Here it is not. This is because .Net 4 only supports xslt and xpath 1.0! So this is the way to do it in 1.0 because 1.0 does not support the extra node tests..

The first template is the copy action, the second template is more interesting. Recall that we put our own xml tags in our Word template. Office 2010 saves those in its own WordML xml as <customXml> tags with element attributes describing its name. The xpath query in the match translates to the following: Only match on w: p nodes which are child’s of w:tc nodes which are childs of customXml nodes whose name is valueHolder. I match the w: p nodes because that is where Word wants the text. If you put text in our Word document within a custom xml tag word puts your text within the w: p node which is a descendant of the custom xml node representing your tag. when we have a match on the w: p node we perform the following steps:

  • Extract the value of our query attribute.Word places it at a rather strange place, as a descendant of the customXml tag with a name and val attribute.
  • We put this in a variable so we can use this later on in our template
  • We recursively copy all the content of the p node to our output document so its completely intact when we add our own text.
  • We add our own value extracted from a .Net object within a couple of Word mark up tags. Line 26 is especially interesting. The function that gets called there gets our xpath expression as a parameter. Our xpath expression was defined with the valueHolder as an attribute in our Word template. So how does this xslt function uses this expression on a .Net object??

Because actually, it&rsquo;s a .Net function being called. And this is super cool, using so called xslt extensions we can call functions on .Net objects from xslt! How cool is this Glimlach. lets look at some code:

       1: namespace Chris.Demo.WordGenerator
       2: {
       3:     /// <summary>
       4:     /// This class will be the heart of our custom Word functionality
       5:     /// </summary>
       6:     class XsltExtension<TMessage>
       7:     {
       8:         XPathNavigator messageDoc;
       9:         public XsltExtension(TMessage message)
      10:         {
      11:             //pre
      12:             Guard.ArgumentsNotNull(message);
      13:&nbsp; 
      14:             XmlSerializer xs = new XmlSerializer(typeof(TMessage));
      15:             MemoryStream ms = new MemoryStream();
      16:             xs.Serialize(ms, message);
      17:             ms.Seek(0, SeekOrigin.Begin);
      18:             messageDoc = new XPathDocument(ms).CreateNavigator();
      19:         }
      20:         /// <summary>
      21:         /// This method will be called from XSLT
      22:         /// </summary>
      23:         /// <param name="xpath"></param>
      24:         /// <returns></returns>
      25:         public string GetMessageValueByXpath(string xpath)
      26:         {
      27:             if (string.IsNullOrWhiteSpace(xpath))
      28:             {
      29:                 return string.Empty;
      30:             }
      31:             else
      32:             {
      33:                 XPathNodeIterator ni = messageDoc.Select(xpath);
      34:                 ni.MoveNext();
      35:                 return ni.Current.ToString();
      36:             }
      37:&nbsp; 
      38:         }
      39:     }
      40: }

This class functions as the base for all of our extra xslt functions, for now there will only be one: GetMessageValueByXpath. This function is called from xslt to extract a value from a .Net object. The .Net object is supplied in the constructor of this extension, it is serialized to xml and converted to a XPathDocument, Once we have the navigator of the XpathDocument we can use every Xpath 1.0 function from our word template, including number formatting, date formatting and even our own defined xpath functions. For this example the object being passed into the xslt extension will be a PersonInfo object corresponding with the properties being specified in our Word template.

Lets the code that creates this extension and passes it to the xslt transform:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using Chris.Demo.WordGenerator.Contract;
   6: using System.Xml.Xsl;
   7: using System.Reflection;
   8: using System.Xml;
   9: using Chris.Demo.WordGenerator.Util;
  10: using System.IO;
  11:&nbsp; 
  12: namespace Chris.Demo.WordGenerator
  13: {
  14:     /// <summary>
  15:     /// A transformer that transforms the objects using XSLT
  16:     /// </summary>
  17:    class XslTransformer:IObjectDocumentTransformer
  18:     {
  19:        private XslCompiledTransform _xsltransform;
  20:        public XslTransformer()
  21:        {
  22:            _xsltransform = new XslCompiledTransform(false);
  23:            Stream stylesheet = Assembly.GetExecutingAssembly().GetManifestResourceStream(Constants.XSLTRESOURCE);
  24:            _xsltransform.Load(XmlReader.Create(stylesheet));
  25:        }
  26:         public System.IO.MemoryStream TransForm<TMessage>(TMessage message, System.IO.Stream template)
  27:         {
  28:             XmlReader templatedoc=XmlReader.Create(template);
  29:             MemoryStream returnStream = new MemoryStream();
  30:             XsltArgumentList arguments = new XsltArgumentList();
  31:             arguments.AddExtensionObject(Constants.DEMONAMESPACE, new XsltExtension<TMessage>(message));
  32:             _xsltransform.Transform(templatedoc, arguments, returnStream);
  33:             returnStream.Seek(0, SeekOrigin.Begin);
  34:             return returnStream;
  35:         }
  36:     }
  37: }

Line 31 is the line that creates the XsltExtension, passes it the message object (PersonInfo in our demo app that will follow), and transforms the passed template with our xslt stylesheet and our extension to a valid word document. The xslt extension needs to be in a separate xml namespace. The DEMOSPACE constant I am using here maps to:urn:Chris:demo:word. In our xslt stylesheet you can see I prefixed this same namespace with wordgen. That&rsquo;s why in the xslt stylesheet the function is called like wordgen:GetMessageValueByXpath($query).

This XslTransformer will be used by the WordGenerator class. The WordGenerator class is the class client applications will interact with. Its code is shown below:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using Chris.Demo.WordGenerator.Contract;
   6: using System.Xml.Xsl;
   7: using System.Xml;
   8: using Chris.Demo.WordGenerator.Util;
   9: using System.IO;
  10: using System.Reflection;
  11: using Conditions = System.Diagnostics.Contracts;
  12:&nbsp; 
  13: namespace Chris.Demo.WordGenerator
  14: {
  15:     /// <summary>
  16:     /// Our generator
  17:     /// </summary>
  18:     public class WordGenerator :BaseGenerator
  19:     {
  20:         public WordGenerator():this(new XslTransformer())
  21:         {
  22:         }
  23:         public WordGenerator(IObjectDocumentTransformer transformer):base(transformer)
  24:         {
  25:         }
  26:&nbsp; 
  27:         /// <summary>
  28:         /// Later there will be more functionality here, for now a very basic implementation
  29:         /// </summary>
  30:         /// <typeparam name="TMessageObject"></typeparam>
  31:         /// <param name="message"></param>
  32:         /// <param name="template"></param>
  33:         /// <returns></returns>
  34:         public override System.IO.MemoryStream GenerateLetter<TMessageObject>(TMessageObject message, Stream template)
  35:         {
  36:             //pre didnt use code contracts because of the extra req tooling
  37:             Guard.ArgumentsNotNull(message, template);
  38:             return Transformer.TransForm(message, template);
  39:         }
  40:     }
  41: }

It inherits functionality from its base class, functionality like the Transformer property and a GenerateAndSave file method which uses the overridden method here. For now it only adds a guard and delegates to the transformer object. Default it will use our xslt transformer but the constructor also accepts another transformer as long as it implements the IDocumentTransformer interface. From this code it is obvious that client applications who will be using this dll, must supply a .Net object that contains the data which you want to be shown on the Word letter and, the client app must supply a Word template. Each client app, or even different parts from one application can generate a lot of different Word letters using this approach, all they have to do is define a template, pass it and the data to the generator and we have a Word letter!

I created a demo app which uses the Word template we have defined earlier. It is a very basic Windows Form that asks for the info we wanted to show in the Word letter, and when we push the button it generates the letter. Lets have a look Glimlach

image

We fill in the data, we press generate and the screen below will show:

image

It asks us where to save the document. I choose my Desktop and presto !Glimlach We have a Word document, generated according to our template with the data we specified in ourForm.

image

Lets see how this works. Here is our Windows Form class, its very simple:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.ComponentModel;
   4: using System.Data;
   5: using System.Drawing;
   6: using System.Linq;
   7: using System.Text;
   8: using System.Windows.Forms;
   9: using Chris.Demo.WordGenerator;
  10: using System.IO;
  11: using FEWordGenDemo.Properties;
  12: using Chris.Demo.WordGenerator.Contract;
  13:&nbsp; 
  14: namespace FEWordGenDemo
  15: {
  16:     public partial class Form1 : Form
  17:     {
  18:         private PersonInfo pi = new PersonInfo();
  19:         private ILetterGenerator _generator;
  20:         public Form1(ILetterGenerator generator)
  21:         {
  22:             InitializeComponent();
  23:             personInfoBindingSource.DataSource = pi;
  24:             _generator = generator;
  25:         }
  26:&nbsp; 
  27:         private void btnGenerate_Click(object sender, EventArgs e)
  28:         {
  29:             if (svfWordDoc.ShowDialog(this)==DialogResult.OK)
  30:             {
  31:                 Stream templateStream = new MemoryStream(Resources.WordTemplate);
  32:                 _generator.GenerateAndSaveLetter(pi, templateStream, svfWordDoc.FileName, true);
  33:             }
  34:         }
  35:     }
  36: }

The most exciting stuff happens from line 27 and downward. Here you will see:

  • The SaveFile Dialog.
  • Getting the Word template, in my app it was embedded as resource but imagine a scenario where you can download all the templates from a location. Whenever the templates need updating, you place them on the server and every client will download it automatically. The last scenario is very similar to a project I have worked on.
  • Asking our Word generator to generate a Word document save it and open it in the default application! You can see I use the constructor to inject the generator, this way I can easily switch implementations. I pass the generator our template as a stream, and the PersonInfo object which should contain al the data by use of databinding.

Conclusion

Xslt, xpath and xsd&rsquo;s in combination with the programming language of our choice enables us to very easily template and generate all kinds of xml documents. Here we have looked at Word generation. Some of you might wonder, why this way, why not just grab the Open Xml sdk to generate Word 2010 documents. This is a valid point, but generating our Word documents this way allows us to easily add extra functionality, think about Conditional place holders, all the xpath formatting that can be done from our templates, Iterative place holders that will generate rows based on a .Net array, stuff like that!

Xslt extensions in .Net are very powerful, you can even pass nodes from xslt to .Net and vice versa allowing us to so some very cool modifications on our message object before sending back to xslt and injecting it in the Word document. This also allows for a very standard way of generating documents as it can be applied to all kinds of xml documents.

All the files I used and created are in one Visual Studio solution. here is the download link:

Pfew! this was a big post with lots of info. I hope you liked reading it. Feel free to comment!

Greetings

Chris

Share this

Figured out how to use content controls to generate documents and how to populate data from an XML into content controls. I’ve divided this into 2 parts:

  • Part 1: Create your template document for document generation
  • Part 2: Use code in C# to generate documents based on template

Part 1: Create your template document for document generation

  1. Create a sample XML based on which you can create the Word template for document generation. Preferably start with a less complicated version to get the hang of it.

I used the following XML for testing. For testing I didn’t have repeating sections, pictures etc.

<?xml version="1.0" encoding="utf-8"?>
<mydata xmlns="http://CustomDemoXML.htm">
    <field1>This is the value in field1 from the XML file</field1>
    <field2>This is the value in field2 from the XML file</field2>
    <field3>This is the value in field3 from the XML file</field3>
</mydata>

Note 1: This is will be just a sample XML to create your Word template. XML file(s) with real data in this same format can later be applied when generating Word document(s) from the template.

Note 2: The xmlns attribute can contain literally anything you want and it doesn’t have to be a URL starting with http.

Save your sample XML file to any location so that it can be imported to the template you are about to create.

  1. Make sure the Developer tab is enabled on your copy of Word [File -> Options -> Customize Ribbon -> Under Customize the Ribbon, make sure Developer is selected -> OK]. Details: How to: Show the Developer Tab on the Ribbon

  2. Create a new Word document (or use an existing Word document) which will be your template for document generation.

  3. On the Developer tab, click on XML Mapping Pane. This will open the XML Mapping Pane on the right side of the document.

  4. On the XML Mapping Pane, select the Custom XML Part drop down -> Select (Add new part).

  5. Select the XML file that you saved on step 1 -> Open.

  6. On the XML Mapping Pane, select the Custom XML Part drop down -> Select the item with the text that was on the xmlns attribute of the custom XML file. If you use the sample file above, it would be http://CustomDemoXML.htm.

  7. Add a some static text to a Word document and add a Plain Text Content Control next to it (on the Developer tab -> Controls section. Repeat for all fields you need to add.

For the sample XML above, I had the following Word document:

sample word document template

  1. Click on the first Plain Text Content Control -> On the XML Mapping Pane, right click the field you want mapped to that content control -> Click Map to Selected Content Control. Repeat for all the fields you want to map.

Note: Alternatively, instead of adding the Plain Text Content Control items from the developer tab on step #8, you could right click on the field you want to map on the XML Mapping Pane -> Click Insert Content Control -> Click Plain Text.

Similarly, you can also add other types of controls such as checkboxes, date pickers and even repeating sections (it supports nested repeating sections too! — since Word 2013) and map data from XML to those using just native Word functionality and without any third party tools!

  1. Save your template document.

Part 2: Use code in C# to generate documents based on template

This uses Microsoft’s recommended OpenXML SDK to generate documents using an XML file containing real data.

  1. Build your XML file/open an existing XML file with which to generate a document from the template created above. This needs to be in the same format as the sample XML file used to create the template.

  2. Use the OpenXML SDK to delete any CustomXMLPart elements from the document. This assumes no other custom XML parts are used in the document which is the case in this example. For complex scenarios, you can delete specific XML parts if needed.

  3. Use the OpenXML SDK to add a new CustomXMLPart based on the XML file in step#1 above.

Here is the sample code I have to «refresh»/»reload» the sample data in the template with a data from an XML file containing real data (assuming the XML file used to generate the document is already created and saved):

using System.IO;
using DocumentFormat.OpenXml.Packaging;

namespace SampleNamespace
{
    public static class SampleClass
    {
        public static void GenerateDocument()
        {
            string rootPath = @"C:Temp";
            string xmlDataFile = rootPath + @"MyNewData.xml";
            string templateDocument = rootPath + @"MyTemplate.docx";
            string outputDocument = rootPath + @"MyGeneratedDocument.docx";

            using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(templateDocument, true))
            {
                //get the main part of the document which contains CustomXMLParts
                MainDocumentPart mainPart = wordDoc.MainDocumentPart;

                //delete all CustomXMLParts in the document. If needed only specific CustomXMLParts can be deleted using the CustomXmlParts IEnumerable
                mainPart.DeleteParts<CustomXmlPart>(mainPart.CustomXmlParts);

                //add new CustomXMLPart with data from new XML file
                CustomXmlPart myXmlPart = mainPart.AddCustomXmlPart(CustomXmlPartType.CustomXml);
                using (FileStream stream = new FileStream(xmlDataFile, FileMode.Open))
                {
                    myXmlPart.FeedData(stream);
                }
            }

        }
    }
}

That’s it!

Понравилась статья? Поделить с друзьями:
  • Xsd for excel xml
  • Xps конвертировать в word онлайн
  • Xps в excel программа
  • Xps в excel онлайн
  • Xps to word конвертер скачать