"Дедушка RTF" еще послужит

OSzone.net » Программы » Обзоры программ » Различные утилиты » "Дедушка RTF" еще послужит
Автор: Арсений Чеботарёв
Иcточник: http://www.cpp.com.ua
Опубликована: 14.03.2005

Мы живем в роскошное время - большинство ресурсов тратится человечеством впустую, буквально на ветер. Это тем более верно для ресурсов компьютерных: типичная загрузка процессора среднего (например, моего) компьютера - что-то около 10%, огромный винчестер завален никому не нужными файлами, из которых вряд ли используется более 20-30%, а до многих очередь так никогда и не доедет, из полутора же гигабайт оперативной памяти я нагружаю, максимум, 600-700 мег.

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

 В качестве иллюстрации можете открыть любой "документ MS Word" с расширением doc и посмотреть, каково соотношение между полезной информацией (это еще предполагая, что напечатанный текст априори является такой информацией) и различной "пургой". На самом деле применение интегрированного COM-формата представления оправдано в одном случае из десяти тысяч. В большинстве же ситуаций вполне достаточно вообще неформатированного текстового представления, с которым справится любой первобытный vim. В других случаях требуется минимум форматирования - вроде автоматического центрирования, отступов и выделения курсивом. Для этих целей вовсе не обязательно таскать за собой COM-storage. Два лучших кандидата на представление "слегка украшенного" текста - это HTML (ака XML-совместимый) и RTF.

 О HTML вы, вероятно, и так уже все знаете, а вот о RTF речь пойдет ниже - и, естественно, не с точки зрения "эникейщика", а с позиции программирования.

 Не станем следовать примеру американских "конвейерных" программистов, уверовавших в огромную пользу всевозможных библиотек и классов, а посему полжизни проводящих: а) в поисках библиотеки, которая делает именно то, что нужно; б) в чатах по поводу "а почему не работает найденная библиотека так-то и так-то".

 Как следствие - мы опускаем (в натуре) такую припару, как RichTextEdit. Помимо прочего, вы все равно не сможете программно ничего сделать "rich" в этом элементе управления без знания RTF. Это не говоря уже о том, что знание сила - и, один раз постигнув, как что-то устроено, вы сможете сколько угодно использовать эти знания.

 Не станем мы использовать и соответствующий класс Java - там тоже один туман, а пользы от этой субстанции не больше, чем от квалификатора final abstract.

 В качестве инструмента мы выберем C# - просто он сейчас под рукой, к тому же дока Visual Studio, MSDN, имеет неплохой референс по RTF. Вообще, DOT NET - это, как по мне, самая удобная иерархия классов за всю историю ООП. Плохо только, что она стоит на COM - а потому не переносима никуда дальше Windows. Однако не нужно думать, что мы используем что-то, зависящее от NET,- напротив, наш "натуралистический" подход позволит переносить идеи на любой язык, существующий в природе, будь то Perl или COBOL, лишь бы он мог выводить текстовые файлы.

 Как и что устроено в RTF

 Итак, что же представляет собой RTF, который мы собрались порвать на куски и скормить нашим программам? По определению это "богатый текст" - в смысле, текст с украшениями в виде форматирования. А на самом-то деле… на самом деле это тоже текст, но не в том смысле, как мы привыкли его воспринимать (и видеть на экране), а в том, что все элементы форматирования - суть текстовые, то есть все символы "printable quotable". Это удобно по многим причинам и роднит RTF с такими языками разметки, как HTML и PostScropt. Одна из "радостей" - это возможность визуальной отладки для программы, то есть возможность собственными глазами увидеть, что делает наша программа. Сделать это не просто, а очень просто - достаточно открыть RTF в любом текстовом редакторе, который не станет интерпретировать символы разметки особым образом,- взять тот же Notepad.

 Что же мы увидим? Хех, нечто вроде показанного ниже:

 {\rtf1\ansi\deflang1049{\fonttbl {\f4\fnil\fcharset204\fprq0 Arial;}
{\f35\fnil\fcharset204\fprq0 Times New Roman;}}{\stylesheet {\s0{\*
\keycode \shift\ctrl N}\snext0\f4\fs20\sl240\slmult1\ql\nowidctlpar
\widctlpar Normal;}}{\info {\*\company Comizdat}{\creatim\yr2004\mo3
\dy19\hr13\min39\sec0}{\author ac2k1}}\viewscale150\margl1701
\margr850\margt1134\margb1134\widowctrl\plain\f35\fs24\pard\f4\fs18
\lang1033 Hello World\par}

 

Как вы поняли (гы-ы), это традиционное приветствие "Hello World" в виде RTF. "Капец! - скажут некоторые из вас.- Какой же это, блин, читабельный текст?!". Ну, это как посмотреть - для папуаса наши книжки тоже 100% все-непонятное, также, как и текст на C++ для непосвященного. Кстати, это еще "милое" представление от текстового редактора Atlantis, а если посмотреть, что делает MS Word (ну, примерно то же, что он делает с HTML) - то там вообще грустная картина.

 С другой стороны в кошмарном виде этого текста нет совершенно ничего страшного, по крайней мере не больше, чем в выражениях RegExp-а. Просто этот текст плохо отформатирован, поскольку никакой редактор не ожидает, что у вас хватит наглости читать RTF в ноутпаде. Попробуем "растопырить" наш RTF и получим что-то вроде:

 {\rtf1\ansi\deflang1049
{\fonttbl
{\f4\fnil\fcharset204\fprq0 Arial;}
{\f35\fnil\fcharset204\fprq0 Times New Roman;}
{\stylesheet
{\s0
{\*\keycode \shift\ctrl N}
\snext0\f4\fs20\sl240\slmult1
\ql\nowidctlpar\widctlpar Normal;
}
}
{\info
{\*\company Comizdat}
{\creatim\yr2004\mo3\dy19\hr13\min39\sec0}
{\author ac2k1}}
\viewscale150\margl1701\margr850\margt1134\margb1134
\widowctrl\plain\f35\fs24\pard\f4\fs18
\lang1033 Hello World\par
}
 

Это уже совсем другое дело. Во-первых, мы видим, что RTF, так же как и XML, имеет иерархическую структуру, то есть документ состоит из элементов, каждый из которых, в свою очередь, также состоит из элементов,- и так далее. Как видно, элементы (группы) заключены в фигурные скобки. В начале документа идет какая-то служебная информация, то есть как бы HEADER.

 Следующее - мы видим подобие атрибутов, которые, правда, хоть и начинаются с "\", но заканчиваются… ничем они не заканчиваются, кроме следующего атрибута или символа конца элемента. Пробельные символы здесь не то чтобы игнорируются - но лучше не разрывать ничего на куски, а аккуратно складывать поэлементно в ровные строчки. Имена атрибутов вполне мнемоничны - например, даже мне, неучу, ясно, что \margt1134 обозначает "верхний отступ 1134 сам-знаешь-чего" (твипов на самом деле, тысячных долей дюйма - то есть отступ равен 1,134 дюйма).

 Все программы по обработке RTF условно делятся на читателей и писателей. Такое различие оправдано, поскольку многие программы просто генерируют свой "аутпут" в этом формате, и их совсем не парит разбирать чужие документы. Писатель, как обычно, "полуграмотный", то есть использует только 3% всего лексикона. Разбор RTF - операция, на порядок более сложная, с учетом всего множества атрибутов. В качестве допустимого минимума читатель должен просто игнорировать непонятные ему команды форматирования, так чтобы, пропустив неясный блок (конечно, при условии, что этот блок корректно упакован в ограничители), продолжить интерпретацию с понятного места. Таким образом, RTF, в общем-то, легко расширяется: можно добавить любые теги, не волнуясь, что это приведет к "непоняткам" с текстом или форматированием. Именно таким образом в RTF можно вставить любые бинарные данные (например, в MIME-кодировке) или специально обрабатываемые элементы (как гиперссылки или поля MS Word).

 Итак, все, в общем-то, понятно, попробуем теперь сгенерировать какой-нибудь RTF.

 Архитектура "писателя"

 Задача генерации текстовых, в том числе вложенных, структур имеет два главных решения, известные по генерации HTML: первое - просто формировать выходной поток как текст (возможно, с небольшой "автоматизацией") - так, как это делает, например, perl или С++; второе - формировать основной шаблон и подставлять в него вычисляемые текстовые макроподстановки, как это делают ASP, PHP, ColdFusion и Delphi.

 Мы применим второй вариант, несколько модифицированный и с такими "фичами":

создать документ
установить глобальные переменные, типа шрифтов,стандартных реквизитов и т.п.
цикл {
установить переменные поля, обычно из БД
получить результат (сохранить, отпечатать, т.п.)
}

В качестве ограничителей макроподстановок будут использоваться угловые скобки - я не нашел их в тексте RTF, и это дает надежду, что они не используются самим RTF. Для файлов будем использовать "<" и ">", для переменных - %имя=значение%. В реальной жизни мы, естественно, будем квотировать эти символы, но пока будем считать, что наш текст не включает таких редких и экзотических символов.

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

 RTFgen rtf=new RTFgen ("main"); \\
rtf.addvar ("text","HELL-O-WORLD");
rtf.generate (savefile);

 Реализация

 После таких детальных установок реализовать полученное довольно просто. Результирующая программа правильно обрабатывает, в основном, корректный ввод - и колбасится от некорректного. Обработка и анализ ошибок - удел коммерческих продуктов, да и то, как правило, со многими глюками и оговорками. Конечно, злобный хакер легко может предположить, что со времени создания нашего объекта, который при создании аккуратно ничего не открывает, некая посторонняя программа подменит наш корневой шаблон, уж не говоря обо всем многообразии возможных ошибок в самом шаблоне,- но отлов этих полутора миллионов вариантов остается вам в качестве домашнего задания.

 Итак, вот как просто выглядит наш класс для генерации RTF-документов на основании иерархических шаблонов:

 using System;
using System.IO;
using System.Collections;
using System.Text;
namespace Catch {
public class RTFgen
{
private string BaseTemp;
private StreamWriter RTFout;
private string path;
private Hashtable variables;
public RTFgen (string root) {
path=Directory.GetCurrentDirectory ()+"
\\Templates\\";
if ((new FileInfo (path+root+".txt")).Exists) {
BaseTemp=root;
variables=new Hashtable ();
}
else
throw (new FileNotFoundException ());
}
public void addvar (string varname, string varvalue) {
variables.Add (varname, varvalue);
}
public void generate (string filename) {
RTFout = File.CreateText (filename);
ProcessFile (BaseTemp);
RTFout.Close ();
}
private void ProcessFile (string Temp) {
string input;
string filename=path+Temp+".txt";
if ((new FileInfo (filename)).Exists) {
StreamReader TempIn=File.OpenText (filename);
while ((input=TempIn.ReadLine ())!=null)
ProcessLine (input);
TempIn.Close ();
}
}
private void ProcessLine (string line2do) {
int pos1=0, pos2=0;
if ((pos1=line2do.IndexOfAny (new char [2]{`<`,`%`}))>-1) {
Out (line2do.Substring (0, pos1));
if (line2do [pos1]==`<`)
if ((pos2=line2do.IndexOf (`>`, pos1+1))>0)
ProcessFile (line2do.Substring (pos1+1, pos2-pos1-1));
if (line2do [pos1]==`%`)
if ((pos2=line2do.IndexOf (`%`, pos1+1))>0)
ProcessVariable (line2do.Substring (pos1+1, pos2-pos1-1));
ProcessLine (line2do.Substring (pos2+1, line2do.Length-pos2-1));
} else
Out (line2do);
}
private void ProcessVariable (string var) {
string varname=var, varvalue="";
int pos1;
if ((pos1=var.IndexOf (`=`))>-1) {
varname=var.Substring (0, pos1);
varvalue=var.Substring (pos1+1, var.Length-pos1-1);
}
Out (variables.ContainsKey (varname)?(string) variables[varname]: varvalue);
}
private void Out (string s){
byte [] bt=Encoding.GetEncoding (1251).GetBytes (s);
foreach (byte b in bt)
RTFout.Write (b<128?((char) b).ToString ():"
\\`"+b.ToString ("x"));
}
}
}
 

Как видите, все вращается вокруг рекурсии. Говоря попросту, обработать файл шаблона - значит обработать его строки одна за одной (ProcessFile). Обработать строку - значит найти в ней первую макроподстановку файла или переменной, затем распечатать начало до макро, сам макрос обработать в зависимости от типа, а остаток строки рекурсивно обработать как отдельную строку. Заметьте, что обработка файла порождает еще один цикл косвенной рекурсии - что, в общем, делает программу весьма интересной.

 Переменные реализованы через хеш-таблицу, так что сами переменные и значения могут быть довольно "странными", по крайней мере с точки зрения традиционных языков: в значении может встречаться знак равенства, поскольку после первого вхождения дальнейшее воспринимается как единая строка. Имена и значения могут содержать пробелы и любые спецсимволы, кроме знаков "=" и "%", в том числе знаки "<" и ">", хотя такое использование позже может вас же ввести в заблуждение, так что злоупотреблять этим не стоит.

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

 Последнее, что осталось тут не освещенным, это собственно шаблон. Мелочь, на которую нужно обратить внимание: не форматируйте шаблоны с помощью красивых отступов табуляцией - некоторые редакторы, вроде MS Word, поймут это как пробелы в тексте, и результат будет не самым лучшим. Впрочем, переносы строки игнорируются всеми "читателями", так что все-таки какой-то приличный вид всегда можно поддерживать, тем более что в шаблоны вы поместите фрагменты одного-двух уровней вложения. Для приведенного выше примера использовался следующий текст main.txt:

 {\rtf1\ansi\ansicpg1251\uc1\deff1\stshfdbch0\stshfloch0\stshfhich0\stshfbi0\deflang1049\deflangfe1049
<fonttbl>
<colortbl>
<stylesheet>
{\*\rsidtbl \rsid5533391}
{\*\generator %Application=RTFgen 1.0%;}
<info>
<pnsec>
<pageset>
{\f48\fs18%text=Hello World%}
}

 

Этот шаблон рессембирует структуру RTF-файла, порождаемого MS Word. Вообще, MS Word, как правило, вставляет секции независимо от того, нужны ли они будут в документе. Например, секция colortabl будет вставлена, даже если в тексте ни единого цветового выделения. Точно так же pnsec описывает "пульки" и отступы для списков до девяти уровней вложения включительно - хотя ваш текст может вообще не включать списков. Насколько это ваш стиль - судите сами, другие редакторы ведут себя более компактно, создавая служебные таблицы динамически на основе реально используемых элементов документа. В конце концов, у вас целых две штатные возможности исключить ненужные вам части: удалить включение файла из шаблона и просто удалить вложенный файл (грубо, но работает без ошибок по определению).

 Сам формат секций объяснять не стану - долго и вообще не особо касается программирования. Самое простое:

Что касается получения полного списка возможностей: поищите строку RTF в MSDN - вас должно интересовать два раздела Header и Document Area, в которых приводятся все теги стандарта RTF 1.6.

 Немного о кодировках

 У современного программирования своя специфика - в виде множества систем кодировки даже для такого изначально простого и детерминированного объекта, как текстовый файл. Чтобы у вас все работало с русским языком, шаблоны должны быть текстовыми файлами Unicode (сигнатура FEFF) - что у вас не получится по умолчанию, если вы воспользуетесь Notepad. Для получения правильного результата с русскими буквами следует принудительно сохранить файл в этом формате. Это нужно делать только для файлов (типично-одного), в котором вы выводите русский текст и делаете подстановки русских макросов.

 Аналогично при выводе - NET всегда будет пытаться вывести текст как UTF8, заменяя не-ANSI символы двухбайтными последовательностями. RTF такого не понимает, он будет воспринимать два байта как два отдельных символа - в результате вывод в текстовом редакторе будет некорректным. Решений, как минимум, три:

private void Out (string s){
byte [] bt=Encoding.GetEncoding (1251).GetBytes (s);
foreach (byte b in bt)
RTFout.BaseStream.WriteByte (b);
}

Вообще-то говоря, эти все проблемы относятся не к NET или нашей программе, а, скорее, к несовершенству мира. Таким же образом вам придется проворачиваться при работе с Java и производными системами, вроде ColdFusion,- там эти проблемы решаются с помощью специальной функции, заставляющей передавать не-ANSI символы одним байтом в терминах кодовых страниц.

 Наглядное представление

 Кстати, несложно также и написать простую программу для форматирования RTF в читабельном виде. Если использование каких-то тегов будет вам неясно из документации, всегда можно реализовать этот эффект в текстовом редакторе - и посмотреть, как это выглядит в терминах RTF. Ниже - текст простого форматировщика, по аналогии с известной утилитой названного RTFnice:

 static void Main (string [] args) {
String s;
Class1 c=new Class1();
while ((s = Console.ReadLine ())!=null)
c.ProcessLine (s,-2);
}
bool firstline=true;
void ProcessLine (string s, int level) {
int pos1, pos2;
if ((pos1=s.IndexOfAny (new Char [2]{`{`,`}`}))>-1) {
if (firstline)
firstline=false;
else
Console.WriteLine (s.Substring (0, pos1));
if (s [pos1]==`{`) {
if ((pos2=s.IndexOfAny (new Char [2]{`{`,`}`}, pos1+1))>-1) {
Shift (level);
if (s [pos2]==`}`) {
Console.Write ("{"+s.Substring (pos1+1, pos2-pos1));
ProcessLine (s.Substring (pos2+1, s.Length-pos2-1), level);
} else {
Console.Write ("{");
ProcessLine (s.Substring (pos1+1, s.Length-pos1-1), level+1);
}
}
}
if (s [pos1]==`}`) {
Console.Write ("}");
ProcessLine (s.Substring (pos1+1, s.Length-pos1-1), level-1);
}
} else
Console.WriteLine (s);
}
void Shift (int n) {for (int i=0; i<=n; i++) Console.Write (`\t`);}
 

Я отнюдь не уверяю, что это самый корректный или наиболее правильный способ форматирования - просто это то, как нравится лично мне, в том числе "листочки" на одной строке, несколько первых уровней без отступов и так далее. Если бы не эти "исключения", программа могла бы быть и проще, и короче. Вызывается эта консольная программа командой вроде:

 RTFnice < file.rtf >out.txt

 По ходу обратите внимание на то, как вызывается нестатический метод из статического: поскольку на момент вызова статического main может не существовать ни одного экземпляра класса, то попытка обращения к его обычным нестатическим методам некорректна - нужно явно создать экземпляр и потом вызывать другие функции нашей программы. Для новичков в NET зачастую это шокирующая новость.

Итого

RTF - хоть и не самый новый, но, тем не менее, мощный и достаточно гибкий формат для документов, переносимых на различные платформы. Если вы заинтересованы в консистентной генерации документов и обмене ими вне зависимости от платформы и без привязки к MS Office - RTF очень даже способен вам в этом помочь. Предлагаемая технология позволяет сравнительно просто и компактно генерировать RTF с помощью несложного кода. Показанный класс RTFgen тривиально портируется на языки, штатно поддерживающие хеш-таблицы, такие как Java или Perl, и чуть сложнее - на те, где таблицы символов придется реализовать самостоятельно. Относительно сложный синтаксис шаблонов и большое количество параметров на поверку оказались мнимыми проблемами - по крайней мере, все оказалось не сложнее HTML, а повторное использование шаблонов может вообще свести вашу работу к минимуму.

 Кстати, сама "машинка" после небольших переделок сможет генерировать и другие структурированные форматы, такие как HTML, XML, PDF, PS и т.д., так что описанные принципы могут иметь самое различное применение.

 На КП-диске выложены образцы шаблонов (реально используемые мной в системе управления публикациями Catch) и сам класс RTFgen.cs. Обратите внимание: каталог Temlates должен находиться уровнем ниже каталога приложения - так что, если вы будете строить приложение в Visual Studio для отладки скопируйте Templates в Debug и\или Release.


Ссылка: http://www.oszone.net/3133/