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.
| Call | Output |
|---|---|
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.goIf 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