<?php
// Чтение текста из RTF
// Версия 0.2
// Автор: Алексей Рембиш a.k.a Ramon
// E-mail: alex@rembish.ru
// Copyright 2009

// Функция, которая проверяет, являются ли данные, с которыми мы сейчас работает
// выводимым на экран текстом. Принцип её работы очень прост - в массиве failAt
// записаны те ключевые слова для текущего состояния стека, которые показывают,
// что перед нами что-то другое, а не текст - например, то могут быть описания
// шрифтов или цветовой палитры. И так далее.
function rtf_isPlainText($s) {
    
$failAt = array("*""fonttbl""colortbl""datastore""themedata");
    for (
$i 0$i count($failAt); $i++)
        if (!empty(
$s[$failAt[$i]])) return false;
    return 
true;
}
function 
rtf2text($filename) {
    
// Пытаемся прочить данные из переданного нам rtf-файла, в случае успеха -
    // продолжаем наше злобненькое дело.
    
$text file_get_contents($filename);
    if (!
strlen($text))
        return 
"";


    
// Итак, самое главное при чтении данных из rtf'а - это текущее состояние
    // стека модификаторов. Начинаем мы, естественно, с пустого стека и отрицательного
    // его (стека) уровня.
    
$document "";
    
$stack = array();
    
$j = -1;

    
// Читаем посимвольно данные...
    
for ($i 0$len strlen($text); $i $len$i++) {
        
$c $text[$i];

        
// исходя из текущего символа выбираем, что мы с данными будем делать.
        
switch ($c) {
            
// итак, самый важный ключ "обратный слеш"
            
case "\\":
                
// читаем следующий символ, чтобы понять, что нам делать дальше
                
$nc $text[$i 1];

                
// Если это другой бэкслеш, или неразрывный пробел, или обязательный
                // дефис, то мы вставляем соответствующие данные в выходной поток
                // (здесь и далее, в поток втавляем только в том случае, если перед
                // нами именно текст, а не шрифтовая вставка, к примеру).
                
if ($nc == '\\' && rtf_isPlainText($stack[$j])) $document .= '\\';
                elseif (
$nc == '~' && rtf_isPlainText($stack[$j])) $document .= ' ';
                elseif (
$nc == '_' && rtf_isPlainText($stack[$j])) $document .= '-';
                
// Если перед нами символ звёздочки, то заносим информацию о нём в стек.
                
elseif ($nc == '*'$stack[$j]["*"] = true;
                
// Если же одинарная кавычка, то мы должны прочитать два следующих
                // символа, которые являются hex-ом символа, который мы должны
                // вставить в наш выходной поток.
                
elseif ($nc == "'") {
                    
$hex substr($text$i 22);
                    if (
rtf_isPlainText($stack[$j]))
                        
$document .= html_entity_decode("&#".hexdec($hex).";");
                    
// Мы прочитали два лишних символа, должны сдвинуть указатель.
                    
$i += 2;
                
// Так перед нами буква, а это значит, что за \ идёт упраляющее слово
                // и возможно некоторый циферный параметр, которые мы должны прочитать.
                
} elseif ($nc >= 'a' && $nc <= 'z' || $nc >= 'A' && $nc <= 'Z') {
                    
$word "";
                    
$param null;

                    
// Начинаем читать символы за бэкслешем.
                    
for ($k $i 1$m 0$k strlen($text); $k++, $m++) {
                        
$nc $text[$k];
                        
// Если текущий символ буква и до этого не было никаких цифр,
                        // то мы всё ещё читаем управляющее слово, если же были цифры,
                        // то по документации мы должны остановиться - ключевое слово
                        // так или иначе закончилось.
                        
if ($nc >= 'a' && $nc <= 'z' || $nc >= 'A' && $nc <= 'Z') {
                            if (empty(
$param))
                                
$word .= $nc;
                            else
                                break;
                        
// Если перед нами цифра, то начинаем записывать параметр слова.
                        
} elseif ($nc >= '0' && $nc <= '9')
                            
$param .= $nc;
                        
// Минус может быть только перед цифровым параметром, поэтому
                        // проверяем параметр на пустоту или в противном случае
                        // мы вылезли за пределы слова с параметром.
                        
elseif ($nc == '-') {
                            if (empty(
$param))
                                
$param .= $nc;
                            else
                                break;
                        
// В любом другом случае - конец.
                        
} else
                            break;
                    }
                    
// Сдвигаем указатель на количество прочитанных нами букв/цифр.
                    
$i += $m 1;

                    
// Начинаем разбираться, что же мы такое начитали. Нас интересует
                    // именно управляющее слово.
                    
$toText "";
                    switch (
strtolower($word)) {
                        
// Если слово "u", то параметр - это десятичное представление
                        // unicode-символа, мы должны добавить его в выход.
                        // Но мы должны учесть, что за символом может стоять его
                        // замена, в случае, если программа просмотрщик не может работать
                        // с Unicode, поэтому при наличии \ucN в стеке, мы должны откусить
                        // "лишние" N символов из исходного потока.
                        
case "u":
                            
$toText .= html_entity_decode("&#x".dechex($param).";");
                            
$ucDelta = @$stack[$j]["uc"];
                            if (
$ucDelta 0)
                                
$i += $ucDelta;
                        break;
                        
// Обработаем переводы строк, различные типы пробелов, а также символ
                        // табуляции.
                        
case "par": case "page": case "column": case "line": case "lbr":
                            
$toText .= "\n"
                        break;
                        case 
"emspace": case "enspace": case "qmspace":
                            
$toText .= " "
                        break;
                        case 
"tab"$toText .= "\t"; break;
                        
// Добавим вместо соответствующих меток текущие дату или время.
                        
case "chdate"$toText .= date("m.d.Y"); break;
                        case 
"chdpl"$toText .= date("l, j F Y"); break;
                        case 
"chdpa"$toText .= date("D, j M Y"); break;
                        case 
"chtime"$toText .= date("H:i:s"); break;
                        
// Заменим некоторые спецсимволы на их html-аналоги.
                        
case "emdash"$toText .= html_entity_decode("&mdash;"); break;
                        case 
"endash"$toText .= html_entity_decode("&ndash;"); break;
                        case 
"bullet"$toText .= html_entity_decode("&#149;"); break;
                        case 
"lquote"$toText .= html_entity_decode("&lsquo;"); break;
                        case 
"rquote"$toText .= html_entity_decode("&rsquo;"); break;
                        case 
"ldblquote"$toText .= html_entity_decode("&laquo;"); break;
                        case 
"rdblquote"$toText .= html_entity_decode("&raquo;"); break;
                        
// Всё остальное добавим в текущий стек управляющих слов. Если у текущего
                        // слова нет параметров, то приравляем параметр true.
                        
default:
                            
$stack[$j][strtolower($word)] = empty($param) ? true $param;
                        break;
                    }
                    
// Если что-то требуется вывести в выходной поток, то выводим, если это требуется.
                    
if (rtf_isPlainText($stack[$j]))
                        
$document .= $toText;
                }

                
$i++;
            break;
            
// Перед нами символ { - значит открывается новая подгруппа, поэтому мы должны завести
            // новый уровень стека с переносом значений с предыдущих уровней.
            
case "{":
                
array_push($stack$stack[$j++]);
            break;
            
// Закрывающаяся фигурная скобка, удаляем текущий уровень из стека. Группа закончилась.
            
case "}":
                
array_pop($stack);
                
$j--;
            break;
            
// Всякие ненужности отбрасываем.
            
case '\0': case '\r': case '\f': case '\n': break;
            
// Остальное, если требуется, отправляем на выход.
            
default:
                if (
rtf_isPlainText($stack[$j]))
                    
$document .= $c;
            break;
        }
    }
    
// Возвращаем, что получили.
    
return $document;
}
?>