Extract Plain Text from RTF in PHP

Last updated: August 25, 2026.

Rich Text Format stores visible text together with control words, nested groups, font tables, color tables, embedded objects, and encoded characters. A full RTF reader is the safest choice for arbitrary documents. The function below is a compact extractor for common text-focused RTF files.

Common RTF elements

  • Braces create groups whose formatting state is inherited by nested groups.
  • Control words begin with a backslash and may have a numeric parameter.
  • \'hh represents a byte in the active Windows code page.
  • \uN represents a Unicode UTF-16 code unit.
  • Destinations such as font tables, pictures, and metadata are not visible body text.

PHP extractor for common documents

<?php
declare(strict_types=1);

function rtfToText(string $rtf): string
{
    $skipWords = array_fill_keys([
        'fonttbl', 'colortbl', 'stylesheet', 'info', 'pict', 'object',
        'header', 'footer', 'fldinst', 'datastore', 'themedata'
    ], true);
    $stack = [];
    $state = ['skip' => false, 'uc' => 1, 'codepage' => 1252];
    $output = '';
    $fallback = 0;
    $length = strlen($rtf);

    for ($i = 0; $i < $length; $i++) {
        $char = $rtf[$i];
        if ($char === '{') { $stack[] = $state; continue; }
        if ($char === '}') { $state = array_pop($stack) ?? $state; continue; }

        if ($char !== '\\') {
            if ($fallback > 0) { $fallback--; continue; }
            if (!$state['skip'] && $char !== "
" && $char !== "
") $output .= $char;
            continue;
        }

        $next = $rtf[$i + 1] ?? '';
        if (str_contains('\\{}', $next)) {
            if (!$state['skip']) $output .= $next;
            $i++; continue;
        }
        if ($next === "'") {
            $hex = substr($rtf, $i + 2, 2);
            if (!$state['skip'] && ctype_xdigit($hex)) {
                $output .= iconv('Windows-' . $state['codepage'], 'UTF-8//IGNORE', chr(hexdec($hex)));
            }
            $i += 3; continue;
        }
        if ($next === '*') { $state['skip'] = true; $i++; continue; }
        if ($next === '~' && !$state['skip']) { $output .= ' '; $i++; continue; }

        if (!preg_match('/\G\\([a-z]+)(-?\d+)? ?/Ai', $rtf, $match, 0, $i)) continue;
        $word = strtolower($match[1]);
        $number = isset($match[2]) ? (int) $match[2] : null;
        $i += strlen($match[0]) - 1;

        if (isset($skipWords[$word])) $state['skip'] = true;
        elseif ($word === 'ansicpg' && $number) $state['codepage'] = $number;
        elseif ($word === 'uc' && $number !== null) $state['uc'] = max(0, $number);
        elseif ($word === 'u' && $number !== null && !$state['skip']) {
            $unit = $number < 0 ? $number + 65536 : $number;
            $output .= mb_convert_encoding(pack('n', $unit), 'UTF-8', 'UTF-16BE');
            $fallback = $state['uc'];
        } elseif (($word === 'par' || $word === 'line') && !$state['skip']) $output .= "
";
        elseif ($word === 'tab' && !$state['skip']) $output .= "	";
    }

    $output = preg_replace('/[ \t]+\n/', "
", $output) ?? $output;
    $output = preg_replace('/\n{3,}/', "

", $output) ?? $output;
    return trim($output);
}

$rtf = file_get_contents(__DIR__ . '/document.rtf');
echo rtfToText($rtf === false ? '' : $rtf);

Limitations

This compact parser does not reproduce tables, list numbering, fields, bidirectional text, surrogate pairs, or every RTF destination. Treat the extracted result as untrusted plain text and HTML-escape it before displaying it on a web page. For document conversion where fidelity matters, use a maintained RTF library or an isolated document-conversion service and test it with files from your users.

admin

admin

2 thoughts on “Extract Plain Text from RTF in PHP

  1. Nice compact code! Works perfectly if you replace the apostrophes with quotation marks in the “Skip trash” line:

    case “\0”: case “\r”: case “\f”: case “\n”:

    Also regards tables if you extend the “Select line feeds, spaces and tabs” lines:

    case “par”: case “page”: case “column”: case “line”: case “lbr”: case “row”:
    $toText .= “\n”;
    break;
    case “emspace”: case “enspace”: case “qmspace”: case “cell”:
    $toText .= ” “;
    break;

Leave a Reply

Your email address will not be published.