ods_reader.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. package office
  2. /*
  3. ods_reader.go - Parse an OpenDocument Spreadsheet (.ods) into a Workbook.
  4. Handles values / strings / booleans, formulas (translated from the ODF
  5. "of:=" syntax back to plain A1 references), cell styles, column widths,
  6. row heights, merges (column/row spans), repeated columns/rows/cells
  7. (capped, like the xlsx reader) and cell notes (office:annotation).
  8. */
  9. import (
  10. "errors"
  11. "strconv"
  12. "strings"
  13. )
  14. // ParseOds converts raw .ods bytes into a Workbook
  15. func ParseOds(data []byte) (*Workbook, error) {
  16. files, mime, err := readOdfZip(data)
  17. if err != nil {
  18. return nil, err
  19. }
  20. if mime != "" && mime != odsMime {
  21. return nil, errors.New("not an OpenDocument spreadsheet (mimetype " + mime + ")")
  22. }
  23. content, ok := files["content.xml"]
  24. if !ok {
  25. return nil, errors.New("ods is missing content.xml")
  26. }
  27. tree, err := parseOdfXML(content)
  28. if err != nil {
  29. return nil, errors.New("cannot parse content.xml: " + err.Error())
  30. }
  31. root := tree.first("document-content")
  32. if root == nil {
  33. return nil, errors.New("content.xml has no document-content root")
  34. }
  35. // automatic styles
  36. cellStyles := map[string]*CellStyle{}
  37. colWidths := map[string]float64{}
  38. rowHeights := map[string]float64{}
  39. if auto := root.first("automatic-styles"); auto != nil {
  40. for _, st := range auto.all("style") {
  41. name := st.attr("name")
  42. if name == "" {
  43. continue
  44. }
  45. switch st.attr("family") {
  46. case "table-cell":
  47. cs := &CellStyle{}
  48. any := false
  49. if tp := st.first("text-properties"); tp != nil {
  50. if tp.attr("font-weight") == "bold" {
  51. cs.B = true
  52. any = true
  53. }
  54. if tp.attr("font-style") == "italic" {
  55. cs.I = true
  56. any = true
  57. }
  58. if v := tp.attr("text-underline-style"); v != "" && v != "none" {
  59. cs.U = true
  60. any = true
  61. }
  62. if c := tp.attr("color"); strings.HasPrefix(c, "#") && strings.ToLower(c) != "#000000" {
  63. cs.Fc = strings.ToLower(c)
  64. any = true
  65. }
  66. if fs := tp.attr("font-size"); strings.HasSuffix(fs, "pt") {
  67. if px := odfLenToPx(fs); px > 0 {
  68. cs.Fs = px
  69. any = true
  70. }
  71. }
  72. }
  73. if cp := st.first("table-cell-properties"); cp != nil {
  74. if bg := cp.attr("background-color"); strings.HasPrefix(bg, "#") {
  75. cs.Bg = strings.ToLower(bg)
  76. any = true
  77. }
  78. if bd := cp.attr("border"); bd != "" && bd != "none" {
  79. cs.Bd = 1
  80. any = true
  81. }
  82. }
  83. if pp := st.first("paragraph-properties"); pp != nil {
  84. switch pp.attr("text-align") {
  85. case "center":
  86. cs.Al = "c"
  87. any = true
  88. case "end", "right":
  89. cs.Al = "r"
  90. any = true
  91. case "start", "left":
  92. cs.Al = "l"
  93. any = true
  94. }
  95. }
  96. if any {
  97. cellStyles[name] = cs
  98. }
  99. case "table-column":
  100. if cp := st.first("table-column-properties"); cp != nil {
  101. if px := odfLenToPx(cp.attr("column-width")); px > 0 {
  102. colWidths[name] = px
  103. }
  104. }
  105. case "table-row":
  106. if rp := st.first("table-row-properties"); rp != nil {
  107. if px := odfLenToPx(rp.attr("row-height")); px > 0 {
  108. rowHeights[name] = px
  109. }
  110. }
  111. }
  112. }
  113. }
  114. ss := root.path("body", "spreadsheet")
  115. if ss == nil {
  116. return nil, errors.New("ods has no spreadsheet body")
  117. }
  118. wb := &Workbook{Sheets: []*WorkSheet{}, Active: 0}
  119. for _, tbl := range ss.all("table") {
  120. ws := &WorkSheet{
  121. Name: tbl.attr("name"),
  122. Cells: map[string]*WorkCell{},
  123. ColW: map[string]float64{},
  124. RowH: map[string]float64{},
  125. }
  126. ci := 0
  127. for _, col := range tbl.all("table-column") {
  128. rep := repeatOf(col.attr("number-columns-repeated"), 64)
  129. w, hasW := colWidths[col.attr("style-name")]
  130. for i := 0; i < rep && ci < 256; i++ {
  131. if hasW && absF(w-xlsxDefColPx) > 1 {
  132. ws.ColW[strconv.Itoa(ci)] = float64(int(w))
  133. }
  134. ci++
  135. }
  136. }
  137. maxCol, maxRow := 0, 0
  138. ri := 0
  139. for _, row := range tbl.all("table-row") {
  140. rowRep := repeatOf(row.attr("number-rows-repeated"), 1024)
  141. if !odsRowHasContent(row) {
  142. ri += rowRep
  143. continue
  144. }
  145. if rowRep > 32 {
  146. rowRep = 32
  147. }
  148. for k := 0; k < rowRep; k++ {
  149. if h, ok := rowHeights[row.attr("style-name")]; ok && absF(h-xlsxDefRowPx) > 1 {
  150. ws.RowH[strconv.Itoa(ri)] = float64(int(h))
  151. }
  152. ci = 0
  153. for _, c := range row.children {
  154. if c.el == nil {
  155. continue
  156. }
  157. cn := c.el
  158. isCovered := cn.name == "covered-table-cell"
  159. if cn.name != "table-cell" && !isCovered {
  160. continue
  161. }
  162. rep := repeatOf(cn.attr("number-columns-repeated"), 256)
  163. if isCovered {
  164. ci += rep
  165. continue
  166. }
  167. for j := 0; j < rep && ci < 1024; j++ {
  168. cell := odsCellOf(cn, cellStyles)
  169. if cell != nil {
  170. ws.Cells[cellRef(ci, ri)] = cell
  171. if ci > maxCol {
  172. maxCol = ci
  173. }
  174. if ri > maxRow {
  175. maxRow = ri
  176. }
  177. }
  178. if j == 0 {
  179. cs := repeatOf(cn.attr("number-columns-spanned"), 256)
  180. rs := repeatOf(cn.attr("number-rows-spanned"), 1024)
  181. if cs > 1 || rs > 1 {
  182. ws.Merges = append(ws.Merges,
  183. cellRef(ci, ri)+":"+cellRef(ci+cs-1, ri+rs-1))
  184. if ri+rs-1 > maxRow {
  185. maxRow = ri + rs - 1
  186. }
  187. }
  188. }
  189. ci++
  190. }
  191. }
  192. ri++
  193. }
  194. }
  195. ws.Cols = maxCol + 6
  196. if ws.Cols < 26 {
  197. ws.Cols = 26
  198. }
  199. ws.Rows = maxRow + 21
  200. if ws.Rows < 200 {
  201. ws.Rows = 200
  202. }
  203. wb.Sheets = append(wb.Sheets, ws)
  204. }
  205. if len(wb.Sheets) == 0 {
  206. return nil, errors.New("no sheets found in ods")
  207. }
  208. return wb, nil
  209. }
  210. func repeatOf(s string, max int) int {
  211. n := atoiSafe(s)
  212. if n < 1 {
  213. return 1
  214. }
  215. if n > max {
  216. return max
  217. }
  218. return n
  219. }
  220. // content = a value, a formula, a span (merge anchor) or an annotation
  221. func odsRowHasContent(row *onode) bool {
  222. for _, c := range row.children {
  223. if c.el == nil || c.el.name != "table-cell" {
  224. continue
  225. }
  226. cn := c.el
  227. if cn.attr("formula") != "" || cn.attr("value-type") != "" ||
  228. cn.attr("number-columns-spanned") != "" ||
  229. cn.attr("number-rows-spanned") != "" || len(cn.children) > 0 {
  230. return true
  231. }
  232. }
  233. return false
  234. }
  235. // odsCellOf converts one table:table-cell to a WorkCell (nil when empty)
  236. func odsCellOf(cn *onode, styles map[string]*CellStyle) *WorkCell {
  237. v := ""
  238. if f := cn.attr("formula"); f != "" {
  239. v = formulaFromOdf(f)
  240. } else {
  241. switch cn.attr("value-type") {
  242. case "float", "currency", "percentage":
  243. v = cn.attr("value")
  244. case "boolean":
  245. if cn.attr("boolean-value") == "true" {
  246. v = "TRUE"
  247. } else {
  248. v = "FALSE"
  249. }
  250. default:
  251. var texts []string
  252. for _, p := range cn.all("p") {
  253. texts = append(texts, p.allText())
  254. }
  255. v = textAsRaw(strings.Join(texts, "\n"))
  256. }
  257. }
  258. note := ""
  259. if an := cn.first("annotation"); an != nil {
  260. note = strings.TrimSpace(an.allText())
  261. }
  262. var st *CellStyle
  263. if cs, ok := styles[cn.attr("style-name")]; ok {
  264. cp := *cs
  265. st = &cp
  266. }
  267. if v == "" && st == nil && note == "" {
  268. return nil
  269. }
  270. return &WorkCell{V: v, S: st, N: note}
  271. }