| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157 |
- /*
- Package doc imports Word 97-2003 documents (.doc) by converting them into
- Word 2007+ documents (.docx), which package docx then reads into the Docs
- model.
- The design follows LibreOffice's Word import (sw/source/filter/ww8): the
- File Information Block (fib.go, WW8Fib), the piece table and formatted
- disk pages (text.go, WW8PLCFx_*), property modifiers (sprm.go,
- wwSprmParser and the ww8par6 attribute handlers), styles and fonts
- (styles.go, WW8RStyle / WW8Fonts), lists (lists.go, WW8ListManager),
- paragraphs and runs (story.go), tables (tables.go, WW8TabDesc), fields
- (emit.go, ww8par5), pictures and drawings (drawing.go, ww8graf) and the
- sub-documents - footnotes, endnotes, comments, headers, text boxes. Where
- LibreOffice builds its own document model, this package writes the
- equivalent WordprocessingML: Word 2007 defined .docx as the XML form of
- the same model, so sprms map onto w:rPr / w:pPr / w:tblPr elements almost
- one to one, and a style's or run's own modifiers stay exactly that.
- Imported: text and character formatting, paragraph formatting, styles,
- bullets and numbering, tables (merged cells, borders, shading, nested
- tables), sections (page size, margins, orientation, columns), headers and
- footers, footnotes and endnotes, comments, hyperlinks and other fields,
- tracked changes, inline and floating pictures (WMF / EMF / PNG / JPEG /
- DIB / TIFF), symbols, and the text of text boxes. Not imported: drawn
- shapes without a picture, OLE objects beyond their preview picture,
- macros, and Word 6.0/95 and older files. Encrypted documents are refused
- with an explanation.
- */
- package doc
- import (
- "errors"
- "sort"
- "imuslab.com/arozos/mod/office/docx"
- "imuslab.com/arozos/mod/office/internal/cfb"
- "imuslab.com/arozos/mod/office/model"
- )
- // docFile is an opened document with everything the writer needs
- type docFile struct {
- wd, tbl, data []byte
- fib *fib
- pieces []piece
- prc [][]byte
- text []uint16
- chpx, papx []fkpRun
- styles *styleSheet
- fonts []font
- lists *lists
- draw *drawings
- revAuthors []string
- }
- var errCorruptFIB = errors.New("the document is damaged: its text counts do not fit the file")
- var errEncrypted = errors.New("this document is password protected; remove the password in Word and save it again to open it here")
- // ToDocx converts a Word 97-2003 document into a .docx file
- func ToDocx(data []byte) ([]byte, error) {
- d, err := open(data)
- if err != nil {
- return nil, err
- }
- return newWriter(d).write()
- }
- // Parse converts a Word 97-2003 document into the Docs model
- func Parse(data []byte) (*model.Document, error) {
- x, err := ToDocx(data)
- if err != nil {
- return nil, err
- }
- return docx.Parse(x)
- }
- func open(data []byte) (*docFile, error) {
- if !cfb.IsCompound(data) {
- return nil, errors.New("not a Word 97-2003 document")
- }
- cf, err := cfb.Open(data)
- if err != nil {
- return nil, err
- }
- if !cf.Has("WordDocument") {
- return nil, errors.New("not a Word document (no WordDocument stream)")
- }
- d := &docFile{}
- if d.wd, err = cf.Stream("WordDocument"); err != nil {
- return nil, err
- }
- if d.fib, err = parseFib(d.wd); err != nil {
- return nil, err
- }
- if d.fib.encrypted {
- return nil, errEncrypted
- }
- tblName := "0Table"
- if d.fib.table1 {
- tblName = "1Table"
- }
- if !cf.Has(tblName) {
- return nil, errors.New("the document's table stream is missing")
- }
- if d.tbl, err = cf.Stream(tblName); err != nil {
- return nil, err
- }
- if cf.Has("Data") {
- d.data, _ = cf.Stream("Data")
- }
- total := d.fib.totalCP()
- // a document with sub-documents has one more paragraph mark after them
- if total > d.fib.ccpText {
- total++
- }
- // every character takes at least a byte of the WordDocument stream: a
- // FIB claiming more is damaged, and would size the text buffer past
- // anything the file holds
- if total > len(d.wd) {
- return nil, errCorruptFIB
- }
- clx := d.fib.span(d.tbl, lcbClx)
- d.pieces, d.prc = pieceTable(clx, total)
- if len(d.pieces) == 0 {
- // no piece table: one 8-bit piece from fcMin (very old writers)
- d.pieces = []piece{{cpStart: 0, cpEnd: total, fc: 0x400, compressed: true}}
- }
- sort.Slice(d.pieces, func(i, j int) bool { return d.pieces[i].cpStart < d.pieces[j].cpStart })
- if last := d.pieces[len(d.pieces)-1].cpEnd; last < total {
- total = last
- }
- d.text = textUnits(d.wd, d.pieces, total)
- d.chpx = readFKPs(d.wd, d.fib.span(d.tbl, lcbPlcfBteChpx), false)
- d.papx = readFKPs(d.wd, d.fib.span(d.tbl, lcbPlcfBtePapx), true)
- d.styles = parseStyles(d.fib.span(d.tbl, lcbStshf))
- d.fonts = parseFonts(d.fib.span(d.tbl, lcbSttbfFfn))
- d.lists = parseLists(d.fib.spanTail(d.tbl, lcbPlfLst), d.fib.spanTail(d.tbl, lcbPlfLfo))
- d.revAuthors = sttb(d.fib.span(d.tbl, lcbSttbfRMark))
- d.readDrawings()
- return d, nil
- }
- // clampCP keeps a CP range inside the text
- func (d *docFile) clamp(cp1, cp2 int) (int, int) {
- n := len(d.text)
- if cp1 < 0 {
- cp1 = 0
- }
- if cp2 > n {
- cp2 = n
- }
- if cp1 > cp2 {
- cp1 = cp2
- }
- return cp1, cp2
- }
|