> Source: https://txtfetch.com/extract/rtf > Plain-text twin — every page on txtfetch.com has one. https://txtfetch.com/text --- # RTF, parsed properly, not regex-stripped. \\uN? Unicode escapes are followed by an ANSI fallback character on purpose. Strip backslashes with a regex, and you duplicate every non-ASCII character in the document. the-problem RTF looks like it should be easy to strip. It's plain ASCII text with backslash control words. That's exactly why so many home-grown parsers get it wrong. Non-ASCII characters are hex-escaped as \\'hh, a byte in the document's code page, set by an \\ansicpg control word like \\ansicpg1252 earlier in the file. Or they're written as \\uN?, a Unicode code point immediately followed by an ANSI fallback character meant for readers that don't support \\u. A regex that just deletes backslash sequences leaves that fallback character behind. So every accented letter, curly quote, or em dash gets duplicated in the output. Embedded objects and images are stored as long hex blobs inline in the control-word stream. A naive stripper will happily interpret those blobs as more 'text' if it isn't specifically built to recognize and skip them. one-request-solution txtfetch runs RTF through Apache Tika's actual RTF parser. It tracks the code page from \\ansicpg, and resolves \\'hh hex escapes and \\uN? Unicode-plus-fallback pairs correctly, keeping the Unicode character and dropping the fallback. It also recognizes embedded-object hex blobs as binary data rather than text. The response is clean prose, not a document with every special character doubled. curl ```curl curl -X POST https://api.txtfetch.com/v1/extract \ -H "Authorization: Bearer $TXTFETCH_KEY" \ -F file=@signed-agreement.rtf ``` Python ```python import os import requests with open("signed-agreement.rtf", "rb") as f: r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, files={"file": f}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript import { readFile } from "node:fs/promises"; const file = new Blob([await readFile("signed-agreement.rtf")]); const form = new FormData(); form.append("file", file, "signed-agreement.rtf"); const res = await fetch("https://api.txtfetch.com/v1/extract", { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, body: form, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { f, err := os.Open("signed-agreement.rtf") if err != nil { panic(err) } defer f.Close() var body bytes.Buffer writer := multipart.NewWriter(&body) part, err := writer.CreateFormFile("file", "signed-agreement.rtf") if err != nil { panic(err) } if _, err := io.Copy(part, f); err != nil { panic(err) } writer.Close() req, err := http.NewRequest("POST", "https://api.txtfetch.com/v1/extract", &body) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` Or skip the download. Pass a `url` parameter and txtfetch fetches the document server-side: curl ```curl curl -X POST "https://api.txtfetch.com/v1/extract?url=https://example.com/legal/terms-v3.rtf" \ -H "Authorization: Bearer $TXTFETCH_KEY" ``` Python ```python import os import requests r = requests.post( "https://api.txtfetch.com/v1/extract", headers={"Authorization": f"Bearer {os.environ['TXTFETCH_KEY']}"}, params={"url": "https://example.com/legal/terms-v3.rtf"}, ) print(r.json()["extracted_text"]) ``` JavaScript ```javascript const endpoint = new URL("https://api.txtfetch.com/v1/extract"); endpoint.searchParams.set("url", "https://example.com/legal/terms-v3.rtf"); const res = await fetch(endpoint, { method: "POST", headers: { Authorization: `Bearer ${process.env.TXTFETCH_KEY}` }, }); const { extracted_text } = await res.json(); console.log(extracted_text); ``` Go ```go package main import ( "encoding/json" "fmt" "net/http" "net/url" "os" ) type extractResponse struct { Status string `json:"status"` ExtractedText string `json:"extracted_text"` } func main() { endpoint, err := url.Parse("https://api.txtfetch.com/v1/extract") if err != nil { panic(err) } q := endpoint.Query() q.Set("url", "https://example.com/legal/terms-v3.rtf") endpoint.RawQuery = q.Encode() req, err := http.NewRequest("POST", endpoint.String(), nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+os.Getenv("TXTFETCH_KEY")) resp, err := http.DefaultClient.Do(req) if err != nil { panic(err) } defer resp.Body.Close() var result extractResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { panic(err) } fmt.Println(result.ExtractedText) } ``` ``` { "status": "success", "extracted_text": "..." } ``` what-comes-back The corpus behind [/diff](https://txtfetch.com/diff) has no recorded .rtf document yet, so we have nothing honest to show you here. Run one of your own instead. The [free converter](https://txtfetch.com/tools/file-to-text) reads the file in your browser, and nothing is uploaded. formats-covered - `.rtf` faq **Why do accented characters or curly quotes appear twice in my extracted RTF text?**: That's the classic symptom of naive backslash-stripping. RTF's \uN? Unicode escape is followed by a plain-ASCII fallback character by design. A regex that just deletes backslash sequences leaves the fallback character behind next to the real one. A real RTF parser resolves the pair correctly instead. **Does character encoding vary between RTF files?**: Yes. The code page for \'hh hex escapes is set per-document by a \ansicpg control word, commonly \ansicpg1252 for Windows-1252. So the same hex byte can mean a different character in different RTF files. Tika reads the declared code page rather than assuming one. **What happens to embedded images or OLE objects in an RTF file?**: They're stored as hex-encoded binary blobs inline in the RTF stream. Tika's parser recognizes and skips them as binary data rather than attempting to read them as text. go-further - [Drop a real .rtf and see the parsed text — free, in your browser →](https://txtfetch.com/tools/file-to-text) - [API quickstart →](https://txtfetch.com/docs) - [How the pipeline works →](https://txtfetch.com/how-it-works) - [Extract it from your language →](https://txtfetch.com/for) - [Get an API key →](https://app.txtfetch.com/signup) Office - [`.docx`](https://txtfetch.com/extract/docx) - [`.pptx`](https://txtfetch.com/extract/pptx) - [`.xlsx`](https://txtfetch.com/extract/xlsx) - [`.doc`](https://txtfetch.com/extract/legacy-office) - [All formats →](https://txtfetch.com/extract) ## Send a real .rtf through it. One HTTP call returns the text. Read one in your browser first, for free. [Get an API key →](https://app.txtfetch.com/signup) [Open the free .rtf reader →](https://txtfetch.com/tools/file-to-text)