mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-rtf: WordCraft's RTF reader and writer on craft-wordmodel and craft-flow (feature doc) * Add craft-wordmodel: WordCraft's document model without its editing code WordCraft's crates/doc (wordcraft-doc, at 07f97737) moved as it is, so the DOCX, RTF, ODT and other text-format crates can read into and write from one model. Module paths, type names and serde attributes are unchanged: an app that re-exports the crate keeps its native file format. Left in the app: range editing (edit.rs), IME text and equation editing. Not model, and already in craft-libs: code pages (craft-rtf), the ZIP entry-count check (craft-opc), the compound-file guard (craft-cfb). The paragraph walks moved to walk.rs and the unit helpers of wordcraft-geom to geom.rs. * craft-wordmodel: math_tree, the arguments of an equation node and a walk over its runs The DOCX reader and writer walk an equation's runs. The walk and the child accessors it uses were in WordCraft's equation editor (math_edit.rs), which stays in the app; they are structure, not editing. * craft-wordmodel: keep the document operations (edit.rs) with the model The DOCX and RTF readers use them: alternative-format chunks are merged into a document with adopt_blocks, and a reader formats ranges with format_range and inserts objects. They operate on the document, not on a view, so they belong beside it. The tests that drove them come back with them; the paragraph walks stay in walk.rs and edit re-exports them. base64 0.23, as WordCraft and craft-offcrypto use. * Add craft-flow: the flow model the text formats map to, on craft-wordmodel WordCraft's crates/formats/src/model (07f97737) moved as it is: Flow, FBlock, Para, Inline, Fmt, tables and cells, pictures, to_doc and from_doc, picture sniffing, colours, base64 and the Dropped report. Only the paths changed (wordcraft_doc is craft_wordmodel); the report helpers are public so the format crates can call them; the image re-encoding helper stays with WordCraft's RTF writer. * wip: WordCraft's RTF interpreter, as it is * Take main's side of files outside craft-rtf * craft-rtf: WordCraft's RTF reader and writer as they are at wordcraft 07f97737 crates/formats/src/rtf.rs and crates/formats/src/rtf/, unchanged and not yet compiled (no module declares them), so the next commit shows only what the move changes. * craft-rtf: WordCraft's RTF reader and writer as craft_rtf::doc (feature doc) On craft-flow and craft-wordmodel, with this crate's code pages (craft-codepage), dates and escaping in place of WordCraft's copies, and craft-metafile's metafile sniffing. A picture in a format RTF cannot carry is re-encoded as PNG with a bounded decode (100 megapixels, 512 MiB), as WordCraft's picture decoder bounded it. The 76 tests moved with it.
112 lines
5.9 KiB
Markdown
112 lines
5.9 KiB
Markdown
# craft-rtf
|
|
|
|
Rich Text Format, read and written. It replaces the RTF code each Craft app carried: the
|
|
tokenizer, code pages, `\u` handling, dates and escaping come from WordCraft's reader and
|
|
writer; the small model covers what DesignCraft's text import, DeckCraft's outline import and
|
|
export and PdfCraft's export read and write.
|
|
|
|
```rust
|
|
use craft_rtf::{Block, CharFmt, Document, Para, Run, read, write};
|
|
|
|
let bold = CharFmt { bold: true, ..CharFmt::default() };
|
|
let doc = Document {
|
|
blocks: vec![Block::Para(Para {
|
|
runs: vec![Run { text: "Plan {Q3} ".into(), fmt: bold }, Run { text: "café 😀".into(), fmt: CharFmt::default() }],
|
|
outline_level: Some(0),
|
|
..Para::default()
|
|
})],
|
|
pictures_left_out: 0,
|
|
};
|
|
let rtf = write(&doc, "Liberation Sans");
|
|
assert!(rtf.contains(r"\outlinelevel0 {\b Plan \{Q3\} }caf\u233?"));
|
|
assert_eq!(read(rtf.as_bytes()).unwrap(), doc);
|
|
```
|
|
|
|
## Layers
|
|
|
|
| Layer | What it is | For |
|
|
| --- | --- | --- |
|
|
| `Tokens` | The tokens of a file: `{`, `}`, control words with their parameter, `\*`, `\'hh`, control symbols, text runs, `\bin` data. | Any reader. |
|
|
| `TextDecoder`, `codepage` | 8-bit text in a code page, `\uN` and its fallback characters. | Any reader. |
|
|
| `esc`, `esc_font`, `date_words`, `iso_to_dttm` | Escaping and dates. | Any writer. |
|
|
| `read`, `write`, `Document` | A file to and from a small model. | An app that needs text with basic formatting. |
|
|
|
|
A reader that keeps more than the model holds (styles, lists, fields, notes, tracked changes,
|
|
pictures) uses the first two layers and its own interpretation of the control words.
|
|
|
|
## Text
|
|
|
|
- **Code pages.** 8-bit text is in the current font's code page (`\fcharset`, `\cpg`), else
|
|
the default font's (`\deff`), else the document's (`\ansicpg`, `\mac`, `\pc`, `\pca`), else
|
|
Windows-1252. Single-byte: Windows 874 and 1250 to 1258, DOS 437, 737, 850, 852, 857, 866,
|
|
Mac OS Roman. Multibyte: 932 (Shift-JIS), 936 (GBK), 949 (UHC), 950 (Big5), streamed through
|
|
one decoder so a lead byte and its trail byte decode together whatever falls between them.
|
|
Raw bytes above 0x7F are text in the code page, as `\'hh` is. They are not read as UTF-8.
|
|
- **`\uN`.** One UTF-16 code unit, taken modulo 65,536. A high surrogate pairs only with a low
|
|
surrogate in the very next `\u`; a surrogate that does not pair is U+FFFD, as in Word. The
|
|
`\ucN` characters after a `\u` are its fallback and are skipped: a text byte, a `\'hh`, a
|
|
control word and a control symbol each count as one, a following `\u` does not, and the
|
|
skipping ends with the group.
|
|
- **Malformed input.** `\'` without two hex digits is dropped with the hex digits it has; a
|
|
`}` or `\` after it keeps its meaning. A control word is at most 32 letters and a parameter
|
|
at most 10 digits. A parameter outside `i32` is clamped.
|
|
|
|
## The model
|
|
|
|
`Document { blocks, pictures_left_out }`, `Block::{Para, Table}`, `Para { runs, outline_level,
|
|
left_indent, right_indent, first_line_indent, space_before, space_after, align }`, `Run { text, fmt }`, `CharFmt { bold, italic, underline, strike,
|
|
vert_align, half_points, color, font }`, `Table { rows: Vec<Vec<Cell>> }`, `Cell { right, paras }`.
|
|
|
|
In `Run::text`, a tab is `\t`, and `LINE_BREAK` (`\n`), `PAGE_BREAK` (U+000C) and
|
|
`COLUMN_BREAK` (U+000E) are `\line`, `\page` and `\column`. Lengths are in twips and sizes in
|
|
half-points, as the file has them.
|
|
|
|
`read` leaves out headers, footers, footnotes, comments, field instructions (a field's result
|
|
is kept), list labels (`\listtext`, `\pntext`), the stylesheet, pictures and objects (counted),
|
|
and every destination marked `\*`. Paragraph properties end with their group, as character
|
|
properties do. A trailing `\par` does not leave an empty last paragraph; empty paragraphs
|
|
elsewhere are kept. `\sect` ends a paragraph.
|
|
|
|
### Limits
|
|
|
|
| Limit | Value | Past it |
|
|
| --- | --- | --- |
|
|
| `MAX_GROUPS` | 512 nested groups | deeper groups are read in the state of the group around them |
|
|
| `MAX_FONTS` | 4096 font table entries | later fonts are not kept |
|
|
| `MAX_COLORS` | 4096 colour table entries | later colours are not kept |
|
|
| `MAX_FONT_NAME` | 256 bytes | the font is not kept |
|
|
|
|
The reader does not recurse, and what it returns is never larger than a small multiple of the
|
|
file: each character of text costs the file at least one byte.
|
|
|
|
## Outlook bodies (feature `outlook`)
|
|
|
|
`outlook::decompress_rtf` undoes the compression of `PidTagRtfCompressed` ([MS-OXRTFCP]: `LZFu`
|
|
with its CRC checked, or `MELA` stored as is), never allocating past the caller's cap.
|
|
`outlook::rtf_text` gives the text a mail body shows (font charsets and `\ansicpg` decoded through
|
|
`craft_mime::charset`, `\uN` with its fallback skipped, destinations dropped), and
|
|
`outlook::rtf_html` the HTML of an encapsulated-HTML body ([MS-OXRTFEX], `\fromhtml1`). Groups nest
|
|
at most `MAX_RTF_DEPTH` (256) deep. From MailCraft `crates/pst/src/rtf.rs` at
|
|
[a44a450](https://github.com/UCS-Care-Group/mailcraft/commit/a44a45013d1c6396229c40ab60172d1882b368d5),
|
|
by Martin Ball and the MailCraft contributors.
|
|
|
|
## The word model (`doc` feature)
|
|
|
|
`craft_rtf::doc` reads RTF into the word model (`craft_wordmodel::Document`) and writes it
|
|
back, through the text formats' flow (`craft_flow`): styles, lists, fields, bookmarks,
|
|
pictures (PNG, JPEG, metafiles; other formats re-encoded as PNG within a bounded decode),
|
|
tables, notes, comments, tracked changes, sections, headers and footers. It is WordCraft's RTF
|
|
reader and writer, on this crate's code pages, dates and escaping.
|
|
|
|
- `doc::import(bytes) -> Result<Document, String>`, `doc::parse(bytes) -> Result<Flow, String>`
|
|
- `doc::export(&Document) -> String`
|
|
|
|
Limits: group nesting (512), picture data (64 MiB a picture), notes (5,000; 500 blocks each),
|
|
sections (10,000), revision authors (10,000), fonts (4,096).
|
|
|
|
## Not here yet
|
|
|
|
`doc` keeps its own tokenizing loop, which has rules the shared `Tokens` does not (`\bin` data
|
|
and fallback characters inside picture data, the destinations a `\uN` fallback is skipped in).
|
|
Moving it onto `Tokens` is a follow-up.
|