Files
llmgw/internal/kb/extract/rtf.go
T
Ilker ManapandClaude Opus 5.5 a0cfafe772 Text from documents of many formats, and chunks of it, for knowledge bases
PDF, Word, Excel, PowerPoint, OpenDocument, RTF, EPUB, HTML, MHTML, mail with
its attachments, notebooks, subtitles, zip archives and text and source files
in any common encoding, in pure Go; legacy Office formats through LibreOffice
and scanned pages through Tesseract when they are installed. Chunks of about
500 tokens under their headings, overlapping by a paragraph.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 13:24:15 +02:00

208 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package extract
import (
"errors"
"strconv"
"strings"
"unicode/utf16"
"golang.org/x/text/encoding/charmap"
"golang.org/x/text/unicode/norm"
)
// rtfSkip are destinations whose content is not the document's text.
var rtfSkip = map[string]bool{
"fonttbl": true, "colortbl": true, "stylesheet": true, "info": true, "pict": true,
"header": true, "headerl": true, "headerr": true, "headerf": true, "footer": true,
"footerl": true, "footerr": true, "footerf": true, "object": true, "xmlnstbl": true,
"listtable": true, "listoverridetable": true, "rsidtbl": true, "generator": true,
"themedata": true, "colorschememapping": true, "latentstyles": true, "datastore": true,
"filetbl": true, "revtbl": true, "pgdsctbl": true, "fldinst": true, "bkmkstart": true,
"bkmkend": true, "shppict": true, "nonshppict": true, "datafield": true, "blipuid": true,
}
// rtfText reads Rich Text Format: text, paragraphs, tabs and table cells,
// characters in the document's code page (\ansicpg) or as \u escapes;
// font tables, pictures, headers and other destinations skipped.
func rtfText(b []byte) (string, error) {
s := string(b)
if !strings.HasPrefix(strings.TrimSpace(s), "{\\rtf") {
return "", errors.New("not RTF: no {\\rtf header")
}
type state struct {
skip bool
uc int // how many fallback characters follow a \u
}
cp := charmap.Windows1252
stack := []state{{uc: 1}}
var o out
var pending []byte // \'hh bytes, decoded in runs so a code page's pairs stay together
var surrogate rune
flush := func() {
if len(pending) == 0 {
return
}
d, _ := cp.NewDecoder().Bytes(pending)
o.raw(string(d))
pending = pending[:0]
}
skipChars := 0
cur := func() *state { return &stack[len(stack)-1] }
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
case '{':
flush()
stack = append(stack, *cur())
case '}':
flush()
if len(stack) > 1 {
stack = stack[:len(stack)-1]
}
case '\\':
if i+1 >= len(s) {
break
}
n := s[i+1]
switch {
case n == '\'' && i+3 < len(s):
v, err := strconv.ParseUint(s[i+2:i+4], 16, 8)
i += 3
if err != nil || cur().skip {
continue
}
if skipChars > 0 {
skipChars--
continue
}
pending = append(pending, byte(v))
case n == '*':
i++
cur().skip = true
case n == '\\' || n == '{' || n == '}':
i++
if !cur().skip {
flush()
o.raw(string(n))
}
case n == '~':
i++
if !cur().skip {
o.raw(" ")
}
case n == '-' || n == '_':
i++
case n == '\n' || n == '\r':
i++
if !cur().skip {
flush()
o.para()
}
case isLetter(n):
j := i + 1
for j < len(s) && isLetter(s[j]) {
j++
}
word := s[i+1 : j]
k := j
if k < len(s) && (s[k] == '-' || (s[k] >= '0' && s[k] <= '9')) {
k++
for k < len(s) && s[k] >= '0' && s[k] <= '9' {
k++
}
}
arg, hasArg := 0, k > j
if hasArg {
arg, _ = strconv.Atoi(s[j:k])
}
if k < len(s) && s[k] == ' ' {
k++
}
i = k - 1
flush()
if rtfSkip[word] {
cur().skip = true
continue
}
switch word {
case "ansicpg":
cp = codePage(arg)
case "uc":
cur().uc = arg
case "u":
if cur().skip {
continue
}
r := rune(arg)
if r < 0 {
r += 65536
}
switch {
case utf16.IsSurrogate(r) && r < 0xDC00:
surrogate = r
case utf16.IsSurrogate(r):
o.raw(string(utf16.DecodeRune(surrogate, r)))
surrogate = 0
default:
o.raw(string(r))
}
skipChars = cur().uc
case "par", "sect", "page":
if !cur().skip {
o.para()
}
case "line":
if !cur().skip {
o.newline()
}
case "tab":
if !cur().skip {
o.raw("\t")
}
case "cell":
if !cur().skip {
o.tab()
}
case "row":
if !cur().skip {
o.newline()
}
case "emdash":
o.raw("—")
case "endash":
o.raw("–")
case "bullet":
o.raw("•")
case "lquote", "rquote":
o.raw("'")
case "ldblquote", "rdblquote":
o.raw("\"")
}
default:
i++
}
case '\r', '\n':
// Line breaks in RTF source are not text.
default:
if cur().skip {
continue
}
if skipChars > 0 {
skipChars--
continue
}
if c >= 0x80 {
// Unescaped 8-bit text, as some writers leave it.
pending = append(pending, c)
continue
}
flush()
o.raw(string(c))
}
}
flush()
return norm.NFC.String(o.String()), nil
}
func isLetter(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') }