Skip to content
Extract Text from a Document

Extract Text from a Document

Document.ExtractText walks the body and returns a DocText. Its Items field is a flat slice of TextItem, one per run, and each item keeps pointers back to the paragraph, run, hyperlink, table cell and drawing it came from. That is what makes extraction useful for more than a word count: you can find the text and still know it was bold, or that it sat in row 3 column 1.

If you only want a string, DocText gives you two ways to flatten, and they do not produce the same result.

CallOutput
Text()Every non-empty item on its own line, with a trailing newline.
TextWithOptions(ExtractTextOptions{})Runs joined within a paragraph, one line per paragraph, no trailing newline.
TextWithOptions(ExtractTextOptions{RunsOnNewLine: true})One line per run, matching Text() but without the trailing newline.

Text() breaks the line at every run boundary, and a run boundary is wherever the formatting changes. A paragraph reading “Hello bold world.” comes back from Text() as three lines. The same paragraph through TextWithOptions with a zero-value options struct comes back as one. Unless you specifically want run granularity, TextWithOptions is the one that reproduces the paragraphs a reader would recognize.

ExtractTextOptions also carries WithNumbering and NumberingIndent, which put the generated list markers back in front of numbered items. That has its own page: Extract Text with List Numbering.

Reading the text

doc, err := document.Open("document.docx")
if err != nil {
    panic(err)
}
defer doc.Close()

extracted := doc.ExtractText()
fmt.Println(extracted.TextWithOptions(document.ExtractTextOptions{}))

for _, item := range extracted.Items {
    if item.Run != nil && item.Run.RPr != nil && item.Run.RPr.B != nil {
        fmt.Println("bold run:", item.Text)
    }
}

Open unpacks the file into temporary storage, so Close is worth deferring even on a read-only pass. The example itself omits it.

The formatting lives on the raw schema types rather than on a UniOffice wrapper, so item.Run.RPr is a *wml.CT_RPr and every field on it is a pointer. Bold and italic are presence checks (RPr.B != nil), while color and highlight carry a value you read from ValAttr.

TextItem.TableInfo is non-nil for anything inside a table and gives you the CT_Tbl, CT_Row and CT_Tc plus RowIndex and ColIndex. DrawingInfo is non-nil for text lifted out of a shape, and its Width and Height are in EMU, so pass them through measurement.FromEMU before they mean anything.

What gets extracted

Body paragraphs, tables and the paragraphs inside their cells, hyperlink text, simple field results (w:fldSimple), text inside content controls at both block and run level, text inside block-level CustomXml wrappers, and text inside both flavors of text box: the older VML shape (w:pict) and the DrawingML shape carried as AlternateContent.

Nested tables come through too, since cells are walked with the same routine as the body.

Limitations

ExtractText reads the body and nothing else. Headers and footers are separate parts and need the package-level ExtractFromHeader and ExtractFromFooter, which take a *wml.Hdr and a *wml.Ftr rather than the Document:

for _, hdr := range doc.Headers() {
    for _, item := range document.ExtractFromHeader(hdr.X()) {
        fmt.Println(item.Text)
    }
}

Both return a plain []TextItem with no Text() helper, so you join them yourself.

Footnotes and endnotes have no extraction entry point at all. Their text lives in footnotes.xml and endnotes.xml, neither of which ExtractText touches, and there is no ExtractFromFootnote equivalent. A document whose substance sits in its notes extracts as if the notes were not there.

Tracked changes are skipped. Inserted and deleted runs are wrapped in w:ins and w:del, which the schema puts under EG_RunLevelElts, and the extraction walk descends only into the R and Sdt branches of EG_ContentRunContentChoice. The practical result is that on a document with revisions still pending, text somebody added is missing from the output even though Word shows it, and so is text somebody marked for deletion. Accept or reject the revisions before extracting if you need a complete result. See Track Changes for how the markup is produced.

Runs are only reached through two of the branches a run-level element can take. Besides the tracked-change branches above, that leaves out smart tags, run-level CustomXml and the bidirectional w:dir and w:bdo wrappers. Text placed inside any of those is not extracted either. Smart tags are the one likely to matter in practice, since Word writes them on its own.

Items contains entries whose Text is empty: an empty paragraph yields one, and so does a run whose only content is a drawing. Both flatteners drop them, but code iterating Items has to expect them.

Run the example

The example opens document.docx and prints every item with its bold and italic flags, color, highlight, table position and, for the text box, its size in millimeters, then prints the flattened text at the end.

git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/text_extraction
go run main.go

If this is your first time using UniOffice, follow the getting started guide to create an API key and set up your development environment.

View the full source

Sample output

A table cell item, a text box item and the flattened text at the end:

3
Text: Column 1
Bold: false
Italic: false
Row: 0
Column: 1
Shade color: #E7E6E6
--------
13
Text: Hi, I am a Text Box
Bold: false
Italic: false
Height in mm: 54.04247299066626
Width in mm: 93.76369008325126
--------

FLATTENED:
Paragraph 1
Paragraph 2
Table 1
Column 1
Column 2
Row 1
Cell 1-1
Cell 1-2
Paragraph 3
Paragraph 4
Hi, I am a Text Box
Last updated on