Работа с форматом RTF в Delphi

Реализация После таких детальных установок реализовать полученное довольно просто. Результирующая программа правильно обрабатывает, в основном, корректный ввод - и колбасится от некорректного. Обработка и анализ ошибок - удел коммерческих продуктов, да и то, как правило, со многими глюками и оговорками. Конечно, злобный хакер легко может предположить, что со времени создания нашего объекта, который при создании аккуратно ничего не открывает, некая посторонняя программа подменит наш корневой шаблон, уж не говоря обо всем многообразии возможных ошибок в самом шаблоне,- но отлов этих полутора миллионов вариантов остается вам в качестве домашнего задания. Итак, вот как просто выглядит наш класс для генерации 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 описывает "пульки" и отступы для списков до девяти уровней вложения включительно - хотя ваш текст может вообще не включать списков. Насколько это ваш стиль - судите сами, другие редакторы ведут себя более компактно, создавая служебные таблицы динамически на основе реально используемых элементов документа. В конце концов, у вас целых две штатные возможности исключить ненужные вам части: удалить включение файла из шаблона и просто удалить вложенный файл (грубо, но работает без ошибок по определению).
Сам формат секций объяснять не стану - долго и вообще не особо касается программирования. Самое простое:·\ql, \qc, \qr - абзац с выравниванием влево, по центру, вправо; ·\fs18 - размер шрифта в полупунктах (в данном случае 9pt); ·\f48 - ссылка шрифт по номеру в таблице fontbl; ·\b-\b0, \i-\i0, \ul-\ulnone - жирное, наклонное и подчеркнутое начертание соответственно; первый тег начинает действие модификатора, второй завешает.
Что касается получения полного списка возможностей: поищите строку RTF в MSDN - вас должно интересовать два раздела Header и Document Area, в которых приводятся все теги стандарта RTF 1.6.
Немного о кодировках
У современного программирования своя специфика - в виде множества систем кодировки даже для такого изначально простого и детерминированного объекта, как текстовый файл. Чтобы у вас все работало с русским языком, шаблоны должны быть текстовыми файлами Unicode (сигнатура FEFF) - что у вас не получится по умолчанию, если вы воспользуетесь Notepad. Для получения правильного результата с русскими буквами следует принудительно сохранить файл в этом формате. Это нужно делать только для файлов (типично-одного), в котором вы выводите русский текст и делаете подстановки русских макросов.
Аналогично при выводе - NET всегда будет пытаться вывести текст как UTF8, заменяя не-ANSI символы двухбайтными последовательностями. RTF такого не понимает, он будет воспринимать два байта как два отдельных символа - в результате вывод в текстовом редакторе будет некорректным. Решений, как минимум, три:·первый метод (как, собственно, все и сделано в приведенном листинге) - для не-ASCI кодов нужно транслировать из юникода в специфическую кодовую таблицу, после чего заменить каждый не-ASCI символ на специфическую ESC-последовательность. Таким образом работает Athlantis Ocean Mind, хотя MS Word тоже понимает эту нотацию. При этом не забывайте устанавливать нужную кодовую таблицу по умолчанию для документа или изменяйте ее по месту - например, если вы захотите вставить пару турецких фраз в сборник финских анекдотов; ·второй метод - хак, то есть недокументированная возможность. Если русская локализация установлена у вас по умолчанию, то символы вовсе не обязательно эскейпить - просто выведите их "как есть" после перекодировки, это даст экономию размера четыре к одному в длинных документах на национальных языках, но, возможно, не всегда такой текст будет отображаться правильно на других системах. Процедура Out для такой "оптимизации" будет выглядеть следующим образом:

private void Out (string s){

byte [] bt=Encoding.GetEncoding (1251).GetBytes (s);

foreach (byte b in bt)

RTFout.BaseStream.WriteByte (b);

}

·и, наконец, третий вариант - работать в юникоде "от и до", то есть просто выводить строку посимвольно, заменяя по ходу не-ASCI на последовательности \uNNNC (где NNN - десятичное представление символа в юникоде, а C - калька в транслите). Этот способ применяется в последних версиях MS Word, и реализовать его тоже не представляет труда - но при этом придется создавать таблицу транслитерального представления, поскольку автоматически сгенерировать ее штатными средствами не получится.
Вообще-то говоря, эти все проблемы относятся не к 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');}

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

Проверка
Антиспам проверка
Image CAPTCHA
...