jstewmc/rtf

使用 PHP 读取和写入富文本格式 (RTF) 文档

v0.5.2 2021-11-12 20:37 UTC

This package is auto-updated.

Last update: 2024-09-13 02:58:59 UTC


README

CircleCI codecov

rtf

此库使得在 PHP 中处理 (RTF) 文档变得简单。

use Jstewmc\Rtf\{Document, Element};

$document = new Document('{\b foo\b0}');

$document
	->getRoot()         // get the document's root group
	->getFirstChild()   // get the "\b" control word element
	->getNextSibling()  // get the "foo" text element
	->setText('bar');   // replace "foo" with "bar"

echo $document;

上面的示例将生成以下输出(注意,文本 "foo" 已被替换为文本 "bar"

{\b bar\b0}

安装

此库需要 PHP 7.4+

它是跨平台的,我们努力使其在 Windows、Linux 和 OSX 上运行得同样好。

应通过 Composer 安装。为此,将以下行添加到您的 composer.json 文件的 require 部分中,并运行 composer update

{
   "require": {
       "jstewmc/rtf": "^0.5"
   }
}

使用此库

此库很大,很难简要总结。为了使理解更容易,我们将其分为 读取写入保存遍历编辑样式化 RTF。

读取 RTF

您可以从字符串或文件中构建 RTF 文档

use Jstewmc\Rtf\Document;

$document1 = new Document('{\b foo\b0}');

$document2 = new Document('/path/to/file.rtf');

您可以从字符串中构建 RTF 片段

use Jstewmc\Rtf\Snippet;

$snippet = new Snippet('\cxds ing');

无论哪种方式,库都将自动对给定的 RTF 进行词法分析、解析和渲染。

写入 RTF

您可以使用 write() 方法将文档或片段输出为 RTF、HTML 或纯文本字符串

use Jstewmc\Rtf\{Document, Snippet};

$document = new Document('{\b foo\b0}');

echo $document->write();        // prints "{\b foo\b0}"
echo $document->write('rtf');   // prints "{\b foo\b0}"
echo $document->write('text');  // prints "foo"
echo $document->write('html');  // prints "<section style=""><p style="">..."

$snippet = new Snippet('\cxds ing');

echo $snippet->write();        // prints "\cxds ing"
echo $snippet->write('rtf');   // prints "\cxds ing"
echo $snippet->write('text');  // prints ""
echo $snippet->write('html');  // prints ""

当文档或片段被视为字符串时,它将返回 RTF 字符串

use Jstewmc\Rtf\{Document, Snippet};

$document = new Document('{\b foo\b0}');

echo $document;           // prints "{\b foo\b0}"
echo (string) $document;  // prints "{\b foo\b0}"
echo ''.$document;        // prints "{\b foo\b0}"

$snippet = new Snippet('\cxds ing');

echo $snippet;           // prints "\cxds ing"
echo (string)$snippet;   // prints "\cxds ing"
echo ''.$snippet;        // prints "\cxds ing"

保存 RTF

您可以使用文档的 save() 方法将文档保存到文件(片段不支持文件)

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$document->save('/path/to/file.txt');   // puts contents "foo"
$document->save('/path/to/file.rtf');   // puts contents "{\b foo\b0}"
$document->save('/path/to/file.html');  // puts contents "<section style="">..."

默认情况下,save() 方法将假设文件的格式来自目标文件的扩展名(例如,htm[l]rtftxt)。如果您使用非标准文件扩展名,您可以指定文件的格式作为方法的第二个参数

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$document->save('/path/to/file.foo', 'text');  // puts contents "foo"

遍历 RTF

文档和片段由 RTF 元素 组成,就像 HTML 文档由标签组成或 XML 文档由节点组成一样。有四种类型的 RTF 元素:组、控制词、控制符号和文本。要遍历文档或片段,您将在这些元素之间移动。

每个元素都由 Jstewmc\Rtf\Element 命名空间中的 PHP 对象表示。

  • 组和文本元素分别由通用对象 Element\GroupElement\Text 表示。
  • 支持的 控制词和控制符号元素由特定对象表示(例如,加粗控制词 \b 对应于 Element\Control\Word\B 元素,星号控制符号 \* 对应于 Element\Control\Symbol\Asterisk 元素)。
  • 不支持 的控制词和控制符号元素分别由通用对象 Element\Control\WordElement\Control\Symbol 表示。

组有点特别,因为它是 RTF 中唯一可以具有子元素的元素。

每个 文档 都有一个根组,这是文档的最高级元素。RTF 文档中的所有其他元素都是此组的后代。遍历文档始终从根组开始

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$document->getRoot()->getFirstChild();  // returns the "\b " control word

另一方面,片段 可能没有根组。出于各种原因,此库将片段视为其自己的根组

use Jstewmc\Rtf\Snippet;

$snippet = new Snippet('\cxds ing');

$snippet->getFirstChild();  // returns the "\cxds " control word

一个组(根组或非根组)提供了一系列遍历其子元素的方法,这些子元素可能包括也可能不包括其他组。

您可以使用 getFirstChild()getLastChild()getChild() 来访问第一个、最后一个或任何子元素

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$root = $document->getRoot();

$root->getFirstChild();  // returns the "\b" control word
$root->getLastChild();   // returns the "\b0" control word
$root->getChild(1);      // returns the "foo" text element

您可以使用 getControlWords()getControlSymbols() 来查询特定的控制词和符号(第二个参数 parameter 可能是:null,任何参数(默认);false,没有参数;或者 int/string

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$root = $document->getRoot();

$group->getControlWords('b');         // returns "\b" and "\b0" control words
$group->getControlWords('b', 0);      // returns the "\b0" control word
$group->getControlWords('b', false);  // returns the "\b" control word

您可以使用 hasChild() 方法来查询是否存在元素

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$root = $document->getRoot();

$root->hasChild(0);    // returns true
$root->hasChild(999);  // returns false

$root->hasChild(new Text('foo'));  // returns true
$root->hasChild(new Text('bar'));  // returns false

$root->hasChild(new Text('foo'), 0);  // returns false
$root->hasChild(new Text('foo'), 1);  // returns true
$root->hasChild(new Text('bar'), 0);  // returns false

尽管上面的例子使用了根组,但任何组元素都支持这些方法。但是,遍历元素不仅仅局限于组。

您可以使用 getNextSibling()getPreviousSibling() 在任何元素间移动

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$root = $document->getRoot();

$b   = $root->getFirstChild();  // returns the "\b" control word
$foo = $b->getNextSibling();    // returns the "foo" text element
$b0  = $foo->getNextSibling();  // returns the "\b0" control word

$foo === $b0->getPreviousSibling();  // returns true
$b === $foo->getPreviousSibling();   // returns true

您可以使用 getNextText()getPreviousText() 在文本元素间移动

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0 bar\i baz\i0}');

$root = $document->getRoot();

$b   = $root->getFirstChild();  // returns the "\b" control word
$foo = $b->getNextText();       // returns the "foo" text element
$bar = $b->getNextText();       // returns the "bar" text element
$baz = $b->getNextText();       // returns the "baz" text element

$bar === $baz->getPreviousSibling();  // returns true
$foo === $bar->getPreviousSibling();  // returns true

编辑 RTF

此库包含了一些编辑 RTF 的方法。

我们已尽力支持从父-子、Group-Element 关系的任一侧编辑元素。因此,请随意使用对您当前拥有的元素和您要尝试进行的更改最有意义的任一侧。

您可以使用 Group::appendChild()Element::appendTo() 将元素作为最后一个子元素追加到组中

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$bar = new Text('bar');

$document->getRoot()->appendChild($bar);

echo $document;  // prints "{\b foo\b0 bar}"

$bar->appendTo($document->getRoot());

echo $document;  // prints "{\b foo\b0 barbar}"

您可以使用 Group::prependChild()Element::prependTo() 将元素作为第一个子元素预先添加到组中

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$bar = new Text('bar');

$document->getRoot()->prependChild($bar);

echo $document;  // prints "{bar\b foo\b0}"

$bar->prependTo($document->getRoot());

echo $document;  // prints "{barbar\b foo\b0}"

您可以使用 Group::insertChild() 在组中的指定索引处插入元素

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$bar = new Text('bar');

$document->getRoot()->insertChild($bar, 1);

echo $document;  // prints "{\b barfoo\b0}"

您可以使用 Element::insertBefore()Element::insertAfter() 分别在另一个元素之前或之后插入当前元素

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$bar = new Text('bar');

$foo = $document->getRoot()->getChild(1);  // returns the "foo" text element

$bar->insertAfter($foo);

echo $document;  // prints "{\b foobar\b0}"

$bar->insertBefore($foo);

echo $document;  // prints "{\b barfoobar\b0}"

您可以使用 Element::putPreviousSibling()Element::putNextSibling() 分别在当前元素之前或之后插入元素

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$bar = new Text('bar');

$foo = $document->getRoot()->getChild(1);  // returns the "foo" text element

$foo->putNextSibling($bar);

echo $document;  // prints "{\b foobar\b0}"

$foo->putPreviousSibling($bar);

echo $document;  // prints "{\b barfoobar\b0}"

您可以使用 Group::replaceChild()Element::replaceWith() 来替换元素

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$bar = new Text('bar');

$document->getRoot()->replaceChild(1, $bar);

echo $document;  // prints "{\b bar\b0}"

$baz = new Text('baz');

$document->getRoot()->getChild(1)->replaceWith($baz);

echo $document;  // prints "{\b baz\b0}"

最后,您可以使用 Group::removeChild() 来删除元素

use Jstewmc\Rtf\{Document, Element\Text};

$document = new Document('{\b foo\b0}');

$document->getRoot()->removeChild(1);

echo $document;  // prints "{\b \b0}"

设置 RTF 样式

RTF 规范将一个元素的样式合并到一个巨大的“组状态”中。然而,这样的单一状态很难处理。因此,我给每个元素添加了一个样式属性,就像 HTML 元素一样。

一个元素的样式被细分为文档状态、部分状态、段落状态和字符状态。

对于任何元素,以下属性都是可用的

  • 字符状态
    • isBold - 一个标志,表示元素是否加粗(默认为 false)
    • isItalic - 一个标志,表示元素是否倾斜(默认为 false)
    • isStrikethrough - 一个标志,表示元素是否加删除线(默认为 false)
    • isSubscript - 一个标志,表示元素是否为下标(默认为 false)
    • isSuperscript - 一个标志,表示元素是否为上标(默认为 false)
    • isUnderline - 一个标志,表示元素是否下划线(默认为 false)
    • isVisible - 一个标志,表示元素是否可见(默认为 true)
  • 段落状态
    • index - 段落的索引(默认为 0)

诚然,这有点笨拙,但您可以像这样访问元素的样式

use Jstewmc\Rtf\Document;

$document = new Document('{\b foo\b0}');

$document
	->getRoot()         // get the document's root group
	->getFirstChild()   // get the "\b" control word element
	->getNextSibling()  // get the "foo" text element
	->getStyle()        // get the text element's style
	->getCharacter()    // get the style's character state
	->getIsBold();      // returns true

文档将在实例化时进行渲染,并对其元素进行样式设置。但是,对文档的任何更改,如插入或删除元素,都将导致元素父组重新渲染。

富文本格式(RTF)

此库遵循 富文本格式版本 1.5 规范(1997),并添加了对忽略的控制词 \* 的支持。

如果您不熟悉富文本格式(RTF)语言,它相对简单。RTF语言有四个主要组成部分:组、控制词、控制符号和文本。

组是RTF文档的基本构建块。一个组由文本、控制词、控制符号或其它括在花括号(《{》和《}》)中的组组成。

类似于XML文档,一个RTF文档必须有一个根组。在根组内部有一个头部,它是一系列文档格式控制词,如果存在,必须出现在任何文本之前,以及主体,文档的内容。

文档头部必须出现在文档的第一个普通文本字符之前,并且可以包括字体、样式、屏幕颜色、图片、脚注、注释(即,批注)、页眉、页脚、摘要信息、字段和书签,以及文档、节、段落和字符格式化属性。

组可以嵌套。一般来说,组内的格式化只影响该组内的文本,组内的文本继承父组的格式。然而,脚注、注释(即,批注)、页眉和页脚组不继承父组的格式。(为了确保这些组始终格式正确,您应该使用《\sectd》、《\pard》和《\plain》控制词设置这些组的格式为默认,然后添加任何需要的格式。)

控制词

控制词是一个特殊格式的命令,用于在RTF文档中执行操作,例如:插入特殊字符、设置段落格式、设置字符格式等。

控制词的形式如下:\<word>[<delimiter>],其中word是一串少于32个的字母字符串,delimiter是以下之一

  • 一个空格(" ") - 空格被认为是控制词的一部分,不会出现在文档中。然而,任何跟在空格后面的字符,包括空格,都会出现在文档中。
  • 一个数字(0-9)或连字符("-") - 一个数字或连字符指示后面跟有一个数字参数。接下来的数字序列通过一个空格或任何非字母或数字的其他字符分隔。参数可以是正数或负数,通常在-32,767和32,767之间。然而,读者应该接受任何任意的数字字符串作为合法的参数。
  • 除字母或数字之外的任何字符 - 在这种情况下,分隔字符终止控制词,但不是控制词的一部分。

某些控制词的参数(例如,粗体,《\b》)只有两种状态。当这样的控制词没有参数(或有一个非零参数)时,假定该控制词开启了属性。当这样的控制词有一个参数为《0》时,假定关闭了属性。

控制符号

控制符号由一个反斜杠后跟一个单个的非字母字符(即,符号)组成。控制符号通常插入一个特殊字符。例如,控制符号《\~》表示一个非断行空格。

通常,控制符号不取参数。然而,撇号控制符号取一个两位十六进制参数(例如,\'hh)。

文本

文本是任何不是组开、组闭、控制词或控制符号的字符。

特殊的字符,如反斜杠(《\"`)、开括号(《{`)和闭括号(《}`》),必须使用反斜杠字符(《\"`)转义。

行结束

RTF规范指示作者每隔大约255个字符插入换行符和/或回车符,并指示读者忽略它们。相反,应该使用《\line》控制词(以及其他控制词)来控制换行,应该使用《\par》控制词来控制段落。

这个库将忽略一个《未转义》的换行符或回车符。然而,它将处理一个《转义》的换行符或回车符作为隐式的《\par》控制词。

目的地

目的地是一组相关的文本,这些文本可能出现在文档的不同位置(即,“目的地”)。目的地还可以是使用但不应出现在文档中的文本。

对于本库而言,目的地必须由忽略的控制符号\* precedes。

因为这个库不支持任何目的地控制词,所以在将文档格式化为文本或HTML时,所有目的地都被忽略。

支持的控制词和符号

本库仅支持几百个可能的RTF控制词和控制符号的小子集(详细信息请参阅RTF规范1.5Latex2Rtf文档

  • 字符格式控制词
    • \b,加粗
    • \i,斜体
    • \plain,重置字符格式
    • \strike,删除线
    • \sub,下标
    • \super,上标
    • \ul\ulnone,下划线(和取消下划线)
    • \v,可见性
  • 特殊字符控制词
    • \bullet,项目符号字符
    • \chdate,当前日期字符串
    • \chdpa,当前日期字符串,缩写形式
    • \chdpl,当前日期字符串,长格式
    • \chtime,当前时间字符串
    • \emdash,长破折号
    • \emspace,空格
    • \endash,短破折号
    • \enspace,等宽空格
    • \ldblquote,左双引号
    • \line,换行
    • \left,左引号
    • \qmspace,空格
    • \rdblquote,右双引号
    • \rquote,右引号
    • \tab,制表符
    • \u,Unicode转义
  • 段落控制词
    • \par,新段落
    • \pard,重置段落格式

本库不支持以下控制词

  • 图片
  • 对象
  • 表格
  • 边框和阴影
  • 项目符号和编号
  • 等等!

如果这个库遇到它不支持的控制词或控制符号,它将创建一个通用的控制词或控制符号元素,分别是Element\Control\WordElement\Control\Symbol。当将文档格式化为HTML或纯文本时,不支持的控制词被忽略,并且它们的文本出现在文档中,除非该组是目的地。

许可证

本库在MIT许可证下发布。

致谢

2015年2月,我启动了一个需要读取和写入RTF文件的项目。实际上,它需要读取和写入RTF语言的法庭记录扩展文件,RTF-CRE

我找不到一个易于扩展新控制词和控制符号的库。所以,我写了自己的(不管好还是不好哈哈)。

非常感谢以下文章的作者,他们帮助我开始了这个项目