среда, 8 июня 2011 г.

Экспорт данных из DataTable(ADO.Net) с помощью XSLT. Часть 4–RTF.

 

Иногда требуется, чтобы документ был представлен в каком-то редактируемом формате, но не всегда можно быть уверенным в том, что на компьютере, где он будет просматриваться или редактироваться есть необходимое для этих целей программное обеспечение. Обычно для целей редактирования используются офисные пакеты, но не на всех компьютерах они есть. Между тем в операционной системе Windows присутствует программа WordPad, способная отображать довольно сложное содержимое. И хотя возможности ее скромны, тем не менее, она иногда бывает довольно полезной. Вставить в документ таблицу, пользуясь средствами этой программы нельзя(там нет инструментов для работы с таблицами), а вот отобразить и редактировать ее содержимое – вполне.

Основным форматом, с которым работает эта программа является RTF(Rich Text Format), который так же является одним из основных форматов программы MS Word, да и вообще операционной системы Windows, где этот формат является форматом буфера обмена(то есть, если даже скопировать что-то на веб-странице в буфер, то потом можно извлечь оттуда скопированное в RTF).

Как это ни странно, но формат этот не бинарный, а текстовый и документ в этом формате вполне может быть прочитан человеком и даже создан в любом текстовом редакторе, включая блокнот. Единственная (ну почти) проблема этого формата – это то, что он слишком сложен и по этой причине непосредственно с ним работают только программы, а писать код на нем – не есть хорошо.

Рассмотрим простой пример. Создадим в программе WordPad документ следующего содержания

Hello, world!
Preved.

Сохраним его и откроем файл в Блокноте. Текст будет выглядеть следующим образом

{\rtf1\ansi\ansicpg1251\deff0\deflang1033{\fonttbl{\f0\fnil\fcharset0 Calibri;}}
{\colortbl ;\red255\green0\blue0;}
{\*\generator Msftedit 5.41.21.2509;}\viewkind4\uc1\pard\sa200\sl276\slmult1\b\f0\fs22 Hello,\b0  \cf1 world\cf0 !\par
\ul Preved\ulnone .\par
}

При беглом взгляде – полная абракадабра, но если присмотреться, то начинает усматриваться смысл и даже не зная формата можно рассмотреть некоторые закономерности. Весь код заключен в фигурные скобки, так же внутри есть отдельные блоки, которые так же заключены в фигурные скобки. Спецсимволы начинаются с обратного слэша, обычно представляются либо алфавитно-цифровыми символами причем сначала идут буквы, потом индексы, хотя индексы не всегда нужны. Так же могут встречаться одиночные не алфавитно-цифровые символы. (например \*). Такие обозначения как ansi, ansicpg1251? deflang1033, как мне кажется не нуждаются в пояснениях, далее идет два интересных блока, а именно таблица шрифтов и цветовая таблица. Долго о них тоже рассуждать нет смысла, скажу лишь, что в тексте будут ссылки на эти таблицы и индексы цветов и шрифтов. В нашем документе используется только один шрифт и один(отличный от черного) цвет, а вообще, когда их больше, то ссылки на них имеют следующий вид

\cf2 – обозначает, что цвет шрифта второй в таблице
\f3 – шрифт третий в таблице.

То есть ничего сложного. Далее в самом тексте документа можно увидеть слова, которые мы ввели в документ и эти слова окружены группами дескрипторов rtf, некоторые из которых без спецификации непонятны, с другими нет никаких проблем. Перед словом Hello можно увидеть среди прочих \b\f0, нетрудно догадаться, что \b означает жирный текст, а \f0 – самый первый шрифт в таблице, там в таблице он и обозначен так же. Сразу после этого слова мы видим \b0  \cf1. Так же нетрудно понять, что \b0 отменяет жирный шрифт, а \cf1 назначает красный цвет(он первый в таблице, нулевым будет черный, это цвет по умолчанию). Сразу после мы видим \cf0 – возврат к черному цвету. Строка завершается дескриптором \par. Последнее слово окружено дескрипторами \ul и \ulnone, которые означают начало и конец подчеркнутого текста.

Есть правда одна проблема: формат не понимает русского языка, равно как и любого другого, кроме английского. Если создать документ с русским текстом, например со словом “Привет”, то вместо этого слова мы увидим \lang1049\'cf\'f0\'e8\'e2\'e5\'f2. В данном случае первый дескриптор означает русский язык, а далее, каждый символ кодируется с помощью \’n , где n – шестнадцатиричное представление символа в кодировке ansi. Это приводит к тому, что для создания преобразований, в которых будет текст не на английском языке, нам понадобится включать в них скрипты, поскольку средствами XSLT такое преобразование не выполнить.

Создание документа нужной структуры

Теперь, имея первоначальные сведения о формате, можно перейти к процессу построения документа. Естественно, не зная достаточно хорошо формата, а только научившись понимать некоторые его элементы, невозможно “с нуля” построить документ, но это, в принципе, и не понадобится. Можно, конечно, изучить спецификации, ссылки на них есть в статье в википедеии, посвященной RTF, но, как я полагаю, достаточного беглого взгляда на эти документы, чтобы такое желание пропало. Я опишу несколько другой подход, а именно: макет документа будет создаваться программами, умеющими работать с RTF, а вот данные, которые мы хотим отобразить, мы будем формировать в своем приложении, а точнее в XSL-преобразовании.

Как и при рассмотрении экспорта в другие форматы, данные будем отображать в виде таблицы. Для этого нам понадобится RTF документ с примером таблицы, какую мы хотим получить. Как я уже упоминал выше, WordPad не умеет работать с таблицами, поэтому для создания документа надо воспользоваться программой Word и при сохранении документа выбрать формат сохранения. Для самой таблицы можно подобрать стиль(так интереснее), но вместо тестовых данных лучше ввести какие-нибудь метки, которые потом легко будет найти в документе, я обычно пишу одним словом и латинскими буквами, что означает данный текст и обрамляю это слово каким-то заметным образом, например по три звездочки с обеих сторон, ну что-то типа ***documentHeader***. Заведомо не будет проблем с кодировкой, легко найти и понять, что данный элемент означает.

Итак, открываем Word, создаем таблицу 3х3, можно выбрать или настроить стиль, включаем надписи, чтобы было видно где что. У меня получилась вот такая таблица-шаблон, с чередующимся стилем строк.

Таблица

Сохраняем документ в формате RTF. Здесь надо сказать, что RTF, создаваемый Word’ом – это не совсем то, что нам нужно. Эта программа включает много лишнего в документ, эти элементы содержат информацию нужную программе, но для правильного отображения они не нужны, а это значит, что разбираться в документе будет сложнее и сложности эти нам ни к чему. Для того, чтобы минимизировать документ мы откроем его в программе WordPad и снова сохраним.

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

{\rtf1\ansi\ansicpg1251\deff0\deflang1049\deflangfe1049\deftab708{\fonttbl{\f0\froman\fprq2\fcharset204{\*\fname Times New Roman;}Times New Roman CYR;}{\f1\fswiss\fprq2\fcharset204 Calibri;}}
{\colortbl ;\red255\green255\blue255;\red0\green0\blue0;\red0\green255\blue255;\red217\green217\blue217;}
{\*\generator Msftedit 5.41.21.2509;}\viewkind4\uc1\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3

\clcfpat2\clcbpat1\clshdng10000\cellx1843
\clcfpat2\clcbpat1\clshdng10000\cellx5387
\clcfpat2\clcbpat1\clshdng10000\cellx7797

\pard\intbl\qc\cf1\b\f1\fs22 ***Header1***\cell ***Header2***\cell ***Header3***\cell\row

\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3

\clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs\cellx1843
\clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs\cellx5387
\clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx7797

\pard\intbl\cf0\b0 ***R1C1***\cell ***R1C2***\cell ***R1C3***\cell\row

\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3

\clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1843
\clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5387
\clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx7797

\pard\intbl ***R2C1***\cell ***R2C2***\cell ***R2C3***\cell\row\pard\sa200\sl276\slmult1\par
}

Текст, который отображается в ячейках таблицы я выделил жирным шрифтом, а дескрипторы, на которые следует обратить внимание в первую очередь – выделил желтым фоном.

Первое, что бросается в глаза – это то, что после каждого значения ячейки идет дескриптор \cell, сообщающий о завершении ячейки, в конце каждой строки мы видим дескриптор \row. Таким образом, для того, чтобы создать таблицу с нужным количеством столбцов, нам надо будет в каждой строке значения определять именно таким образом. Но это еще не все. Помимо значений нам надо будет указать и другую информацию о ячейках. В первую очередь нас интересует ширина каждой ячейки и стиль(цвет фона и цвет шрифта). Без этого правильно отобразить таблицу не получится.

Перед строкой заголовка мы видим

\clcfpat2\clcbpat1\clshdng10000\cellx1843
\clcfpat2\clcbpat1\clshdng10000\cellx5387
\clcfpat2\clcbpat1\clshdng10000\cellx7797

Это описания для каждой ячейки заголовка, опишу значения важных для нас элементов(тех, которые, возможно, придется изменять).

  • \clcfpat2 – означает, что цвет шрифта ячейки в цветовой таблице второй в списке
  • \clcbpat1 – цвет фона ячейки в цветовой таблице первый в списке
  • \celxN – этот параметр влияет на ширину ячейки, но фактически это не ширина, а расстояние между правой границей ячейки и началом строки таблицы. То есть в каждой следующей ячейке этот параметр увеличивается по сравнению с предыдущими. Вместо N – число, указывающее это расстояние в твипах.

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

Попробуем поэкспериментировать и внесем несколько изменений вручную. Там где описаны определения для заголовка добавим еще одно такое же, только к отступу от начала строки прибавим 2000 твипов

\clcfpat2\clcbpat1\clshdng10000\cellx9797

Аналогичные изменения внесем и в определения других строк. Кроме того в сами строки внесем дополнительные ячейки

\pard\intbl\qc\cf1\b\f1\fs22 ***Header1***\cell ***Header2***\cell ***Header3***\cell ***Header4***\cell\row

И в остальных то же самое. Получится следующий код.

{\rtf1\ansi\ansicpg1251\deff0\deflang1049\deflangfe1049\deftab708{\fonttbl{\f0\froman\fprq2\fcharset204{\*\fname Times New Roman;}Times New Roman CYR;}{\f1\fswiss\fprq2\fcharset204 Calibri;}} {\colortbl ;\red255\green255\blue255;\red0\green0\blue0;\red0\green255\blue255;\red217\green217\blue217;} {\*\generator Msftedit 5.41.21.2509;}\viewkind4\uc1\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3

\clcfpat2\clcbpat1\clshdng10000\cellx1843 \clcfpat2\clcbpat1\clshdng10000\cellx5387 \clcfpat2\clcbpat1\clshdng10000\cellx7797 \clcfpat2\clcbpat1\clshdng10000\cellx9797

\pard\intbl\qc\cf1\b\f1\fs22 ***Header1***\cell ***Header2***\cell ***Header3***\cell ***Header4***\cell\row

\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3

\clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs\cellx1843 \clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs\cellx5387 \clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx7797 \clcbpat1\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx9797

\pard\intbl\cf0\b0 ***R1C1***\cell ***R1C2***\cell ***R1C3***\cell ***R1C4***\cell\row

\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3

\clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx1843 \clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx5387 \clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx7797 \clcbpat4\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs \cellx9797

\pard\intbl ***R2C1***\cell ***R2C2***\cell ***R2C3***\cell ***R2C4***\cell\row\pard\sa200\sl276\slmult1\par }

Сохраним изменения и откроем в WordPad или Word. Вот, что получилось у меня.

TableEdited

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

XSL преобразование

Весь процесс состоит в том, чтобы разделить изготовленный шаблон на неизменяемые части и их перенести в преобразование в виде текстовых блоков, а изменяемые сгенерировать и вставить в нужные места. Но для начала надо оговорить еще один момент, о котором я уже упоминал. Речь о всевозможных нелатинских символах. Основные символы из набора(символы 0-128 всех кодировок) могут передаваться как есть за несколькими исключениями. Ну к примеру, обратный слеш используется как спецсимвол и будет интерпретирован именно так, поэтому для того, чтобы он отобразился в документе его надо удвоить, Другой пример – фигурная скобка, так же используется в разметке, поэтому для ее отображения надо ее экранировать обратным слешем(\{ или \}). Здесь придется внимательно изучить какие символы как должны представляться, выискивать их и заменять. С символами других языков еще больше мороки, поскольку придется проверять какому именно алфавиту они принадлежать и указывать это в разметке, хорошо, если это один язык и заранее известно какой именно. Хотя, если все символы, предназначенные для отображения передавать в виде управляющих последовательностей, то первая проблема( с экранированием спецсимволов) решается автоматически. Такой код не очень компактен, поскольку придется писать по четыре символа для отображения только одного(в случае латинских букв и символов основного набора), зато реализовать проще. Проблему с мультиязычными документами, а так же случаями, когда неизвестно заранее какие языки будут использоваться, можно решить с помощью еще одного способа, а именно последовательности для отображения символов Юникода. К сожалению этот способ еще более громоздкий, поскольку для вывода одного символа потребуется написать до 8-ми, тем не менее он самый надежный и простой в реализации. В любом случае, выбирать следует исходя из ситуации: если размер файла не критичен, то можно отображать символы юникода; если критичен, то лучше воспользоваться всеми способами понемногу. Например латиницу отображать как есть, выбранный язык – при помощи ANSI-последовательностей, а остальное – Юникодом. Символ Юникода отображается так \uN? . Здесь синяя часть должна присутствовать как есть, красная –  код символа в Юникоде(в десятичной системе), в конце символ, который будет отображаться программами, не поддерживающими Юникод. Например последовательность \u1055?\u1088?\u1080?\u1074?\u1077?\u1090? выведет слово “Привет”.

В преобразовании я использовал только метод, переводящий текст в последовательности Юникода, однако для полноты картины вставил метод, переводящий строки в последовательности ANSI, который не используется.

Код преобразования:

1: <?xml version="1.0" encoding="utf-8"?>
2: <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
3:     xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
4:     xmlns:xsd="http://www.w3.org/2001/XMLSchema"
5:     xmlns:custom="urn:schemas-custom">
6:     <xsl:output method="text" indent="no" encoding="us-ascii"/>
7:   <xsl:variable name="columns" select="/*/xsd:schema//xsd:element[count(ancestor::xsd:element) = 2]"/>
8:     <xsl:template match="/">
9:       <xsl:text>{\rtf1\ansi\ansicpg1251\deff0\deflang1049\deflangfe1049\deftab708
10: {\fonttbl{\f0\froman\fprq2\fcharset204{\*\fname Times New Roman;}
11: Times New Roman CYR;}{\f1\fswiss\fprq2\fcharset204 Calibri;}}
12: 
13: {\colortbl ;
14: \red255\green255\blue255;
15: \red0\green0\blue0;
16: \red0\green255\blue255;
17: \red217\green217\blue217;}
18: 
19: {\*\generator Msftedit 5.41.21.2509;}
20: \viewkind4\uc1\trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 
21: \trbrdrt\brdrs\brdrw10 \trbrdrr\brdrs\brdrw10 
22: \trbrdrb\brdrs\brdrw10 \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3
23: </xsl:text>
24:       <!--Определение ячеек заголовка-->
25: 
26:       <xsl:for-each select="$columns">
27:         <xsl:text>\clcfpat2\clcbpat1\clshdng10000\cellx</xsl:text>
28:         <xsl:value-of select="position()*2000"/>
29:         <xsl:text>&#13;&#10;</xsl:text>
30:       </xsl:for-each>
31:       
32:       <xsl:text>\pard\intbl\qc\cf1\b\f1\fs22 </xsl:text>
33:       <!--Строка заголовка-->
34:       <xsl:for-each select="$columns">
35:         <xsl:text> </xsl:text>
36:         <xsl:value-of select="custom:StringToRtfUnicode(@name)"/>
37:         <xsl:text>\cell</xsl:text>
38:       </xsl:for-each>
39:       <xsl:text>\row</xsl:text>
40:       <xsl:apply-templates select="/*/*[position() &gt; 1]"/>
41:       <xsl:text>\pard\sa200\sl276\slmult1\par}</xsl:text>
42:     </xsl:template>
43: 
44:   <xsl:template match="/*/*[position() &gt; 1]">
45:     <xsl:variable name="name" select="name()"/>
46:     <xsl:variable name="cellbgcolor">
47:       <xsl:choose>
48:         <xsl:when test="count(preceding-sibling::*[name() = $name]) mod 2 = 0">
49:           <xsl:text>\clcbpat1</xsl:text>
50:         </xsl:when>
51:         <xsl:otherwise>
52:           <xsl:text>\clcbpat4</xsl:text>
53:         </xsl:otherwise>
54:       </xsl:choose>
55:     </xsl:variable>
56:     <xsl:text>
57: \trowd\trgaph108\trleft-108\trbrdrl\brdrs\brdrw10 \trbrdrt\brdrs\brdrw10 
58: \trbrdrr\brdrs\brdrw10 \trbrdrb\brdrs\brdrw10 
59: \trpaddl108\trpaddr108\trpaddfl3\trpaddfr3
60: </xsl:text>
61:     <xsl:variable name="currentrow" select="."/>
62:     <xsl:for-each select="$columns">
63:       <xsl:value-of select="$cellbgcolor"/>
64:       <xsl:text>\clbrdrl\brdrw10\brdrs\clbrdrt\brdrw10\brdrs\clbrdrr\brdrw10\brdrs\clbrdrb\brdrw10\brdrs\cellx</xsl:text>
65:       <xsl:value-of select="position()*2000"/>
66:       <xsl:text>&#13;&#10;</xsl:text>
67:     </xsl:for-each>
68:     <xsl:text>\pard\intbl\cf0\b0 </xsl:text>
69:     <xsl:for-each select="$columns">
70:       <xsl:variable name="columnname" select="@name"/>
71:       
72:       <xsl:value-of select="custom:StringToRtfUnicode($currentrow/*[name() = $columnname])"/>
73:       <xsl:text>\cell&#13;&#10;</xsl:text>
74:     </xsl:for-each>
75:     <xsl:text>\row</xsl:text>
76:   </xsl:template>
77: 
78:   <msxsl:script language="vb" implements-prefix="custom">
79:     <msxsl:assembly name="System.Core"/>
80:     <msxsl:assembly name="Microsoft.VisualBasic"/>
81:     <msxsl:using namespace="System.Linq"/>
82:     <msxsl:using namespace="Microsoft.VisualBasic"/>
83:     <![CDATA[
84:     Function StringToRtfAnsi(ByVal input As String) As String
85:     Return String.Join( _
86:             "", _
87:             From ch As Char In input _
88:             Let chNum = Asc(ch) _
89:             Select If(chNum < 16, "\'0", "\'") _
90:             & Convert.ToString(chNum, 16))
91:     End Function
92:         
93:     Function StringToRtfUnicode(ByVal input As String) As String
94:        Return String.Join( _
95:         "", _
96:         From ch As String In input _
97:         Select "\u" & AscW(ch) & "?")
98:     End Function
99:     ]]>
100:   </msxsl:script>
101: </xsl:stylesheet>
102: 

В выходном файле будут лишние переходы строк, которые вставлены для наглядности, если размер выходных данных надо минимизировать, то их надо убрать.

Комментариев нет :

Отправить комментарий