doc.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. /*
  2. Package doc imports Word 97-2003 documents (.doc) by converting them into
  3. Word 2007+ documents (.docx), which package docx then reads into the Docs
  4. model.
  5. The design follows LibreOffice's Word import (sw/source/filter/ww8): the
  6. File Information Block (fib.go, WW8Fib), the piece table and formatted
  7. disk pages (text.go, WW8PLCFx_*), property modifiers (sprm.go,
  8. wwSprmParser and the ww8par6 attribute handlers), styles and fonts
  9. (styles.go, WW8RStyle / WW8Fonts), lists (lists.go, WW8ListManager),
  10. paragraphs and runs (story.go), tables (tables.go, WW8TabDesc), fields
  11. (emit.go, ww8par5), pictures and drawings (drawing.go, ww8graf) and the
  12. sub-documents - footnotes, endnotes, comments, headers, text boxes. Where
  13. LibreOffice builds its own document model, this package writes the
  14. equivalent WordprocessingML: Word 2007 defined .docx as the XML form of
  15. the same model, so sprms map onto w:rPr / w:pPr / w:tblPr elements almost
  16. one to one, and a style's or run's own modifiers stay exactly that.
  17. Imported: text and character formatting, paragraph formatting, styles,
  18. bullets and numbering, tables (merged cells, borders, shading, nested
  19. tables), sections (page size, margins, orientation, columns), headers and
  20. footers, footnotes and endnotes, comments, hyperlinks and other fields,
  21. tracked changes, inline and floating pictures (WMF / EMF / PNG / JPEG /
  22. DIB / TIFF), symbols, and the text of text boxes. Not imported: drawn
  23. shapes without a picture, OLE objects beyond their preview picture,
  24. macros, and Word 6.0/95 and older files. Encrypted documents are refused
  25. with an explanation.
  26. */
  27. package doc
  28. import (
  29. "errors"
  30. "sort"
  31. "imuslab.com/arozos/mod/office/docx"
  32. "imuslab.com/arozos/mod/office/internal/cfb"
  33. "imuslab.com/arozos/mod/office/model"
  34. )
  35. // docFile is an opened document with everything the writer needs
  36. type docFile struct {
  37. wd, tbl, data []byte
  38. fib *fib
  39. pieces []piece
  40. prc [][]byte
  41. text []uint16
  42. chpx, papx []fkpRun
  43. styles *styleSheet
  44. fonts []font
  45. lists *lists
  46. draw *drawings
  47. revAuthors []string
  48. }
  49. var errCorruptFIB = errors.New("the document is damaged: its text counts do not fit the file")
  50. var errEncrypted = errors.New("this document is password protected; remove the password in Word and save it again to open it here")
  51. // ToDocx converts a Word 97-2003 document into a .docx file
  52. func ToDocx(data []byte) ([]byte, error) {
  53. d, err := open(data)
  54. if err != nil {
  55. return nil, err
  56. }
  57. return newWriter(d).write()
  58. }
  59. // Parse converts a Word 97-2003 document into the Docs model
  60. func Parse(data []byte) (*model.Document, error) {
  61. x, err := ToDocx(data)
  62. if err != nil {
  63. return nil, err
  64. }
  65. return docx.Parse(x)
  66. }
  67. func open(data []byte) (*docFile, error) {
  68. if !cfb.IsCompound(data) {
  69. return nil, errors.New("not a Word 97-2003 document")
  70. }
  71. cf, err := cfb.Open(data)
  72. if err != nil {
  73. return nil, err
  74. }
  75. if !cf.Has("WordDocument") {
  76. return nil, errors.New("not a Word document (no WordDocument stream)")
  77. }
  78. d := &docFile{}
  79. if d.wd, err = cf.Stream("WordDocument"); err != nil {
  80. return nil, err
  81. }
  82. if d.fib, err = parseFib(d.wd); err != nil {
  83. return nil, err
  84. }
  85. if d.fib.encrypted {
  86. return nil, errEncrypted
  87. }
  88. tblName := "0Table"
  89. if d.fib.table1 {
  90. tblName = "1Table"
  91. }
  92. if !cf.Has(tblName) {
  93. return nil, errors.New("the document's table stream is missing")
  94. }
  95. if d.tbl, err = cf.Stream(tblName); err != nil {
  96. return nil, err
  97. }
  98. if cf.Has("Data") {
  99. d.data, _ = cf.Stream("Data")
  100. }
  101. total := d.fib.totalCP()
  102. // a document with sub-documents has one more paragraph mark after them
  103. if total > d.fib.ccpText {
  104. total++
  105. }
  106. // every character takes at least a byte of the WordDocument stream: a
  107. // FIB claiming more is damaged, and would size the text buffer past
  108. // anything the file holds
  109. if total > len(d.wd) {
  110. return nil, errCorruptFIB
  111. }
  112. clx := d.fib.span(d.tbl, lcbClx)
  113. d.pieces, d.prc = pieceTable(clx, total)
  114. if len(d.pieces) == 0 {
  115. // no piece table: one 8-bit piece from fcMin (very old writers)
  116. d.pieces = []piece{{cpStart: 0, cpEnd: total, fc: 0x400, compressed: true}}
  117. }
  118. sort.Slice(d.pieces, func(i, j int) bool { return d.pieces[i].cpStart < d.pieces[j].cpStart })
  119. if last := d.pieces[len(d.pieces)-1].cpEnd; last < total {
  120. total = last
  121. }
  122. d.text = textUnits(d.wd, d.pieces, total)
  123. d.chpx = readFKPs(d.wd, d.fib.span(d.tbl, lcbPlcfBteChpx), false)
  124. d.papx = readFKPs(d.wd, d.fib.span(d.tbl, lcbPlcfBtePapx), true)
  125. d.styles = parseStyles(d.fib.span(d.tbl, lcbStshf))
  126. d.fonts = parseFonts(d.fib.span(d.tbl, lcbSttbfFfn))
  127. d.lists = parseLists(d.fib.spanTail(d.tbl, lcbPlfLst), d.fib.spanTail(d.tbl, lcbPlfLfo))
  128. d.revAuthors = sttb(d.fib.span(d.tbl, lcbSttbfRMark))
  129. d.readDrawings()
  130. return d, nil
  131. }
  132. // clampCP keeps a CP range inside the text
  133. func (d *docFile) clamp(cp1, cp2 int) (int, int) {
  134. n := len(d.text)
  135. if cp1 < 0 {
  136. cp1 = 0
  137. }
  138. if cp2 > n {
  139. cp2 = n
  140. }
  141. if cp1 > cp2 {
  142. cp1 = cp2
  143. }
  144. return cp1, cp2
  145. }