xlsx_reader.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. package office
  2. /*
  3. xlsx_reader.go - Parse an Excel (.xlsx) file into a Workbook.
  4. Handles the common SpreadsheetML subset: shared + inline strings,
  5. numbers, booleans, formulas, cell styles (bold/italic/underline, font
  6. color/size, fill, alignment, wrap), number formats (mapped back to the
  7. webapp's fmt names), column widths, row heights, merged cells and
  8. frozen panes. Bar/line/pie charts are mapped back to the webapp chart
  9. model (xlsx_charts.go); pivot tables and conditional formatting are
  10. ignored. Legacy binary .xls is rejected up front.
  11. */
  12. import (
  13. "archive/zip"
  14. "bytes"
  15. "errors"
  16. "io"
  17. "path"
  18. "strconv"
  19. "strings"
  20. )
  21. // ParseXlsx converts raw .xlsx bytes into a Workbook
  22. func ParseXlsx(data []byte) (*Workbook, error) {
  23. if len(data) > 8 && data[0] == 0xD0 && data[1] == 0xCF {
  24. return nil, errors.New("legacy binary .xls files are not supported - save the file as .xlsx first")
  25. }
  26. zr, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
  27. if err != nil {
  28. return nil, errors.New("not a valid xlsx (zip) file")
  29. }
  30. files := map[string][]byte{}
  31. for _, f := range zr.File {
  32. name := path.Clean(f.Name)
  33. if strings.HasSuffix(name, ".xml") || strings.HasSuffix(name, ".rels") {
  34. rc, err := f.Open()
  35. if err != nil {
  36. continue
  37. }
  38. b, err := io.ReadAll(rc)
  39. rc.Close()
  40. if err != nil {
  41. continue
  42. }
  43. files[name] = b
  44. }
  45. }
  46. wbXML, ok := files["xl/workbook.xml"]
  47. if !ok {
  48. return nil, errors.New("xlsx is missing xl/workbook.xml")
  49. }
  50. wbTree, err := parseXMLTree(wbXML)
  51. if err != nil {
  52. return nil, errors.New("cannot parse workbook.xml: " + err.Error())
  53. }
  54. rels := parseRels(files["xl/_rels/workbook.xml.rels"])
  55. shared := parseSharedStrings(files["xl/sharedStrings.xml"])
  56. styleMap := parseXlsxStyles(files["xl/styles.xml"])
  57. wb := &Workbook{Sheets: []*WorkSheet{}, Active: 0}
  58. chartIDSeq := 0
  59. if bv := wbTree.path("bookViews", "workbookView"); bv != nil {
  60. if at, err := strconv.Atoi(bv.attr("activeTab")); err == nil {
  61. wb.Active = at
  62. }
  63. }
  64. sheetsNode := wbTree.first("sheets")
  65. if sheetsNode == nil {
  66. return nil, errors.New("workbook has no sheets")
  67. }
  68. for _, sn := range sheetsNode.all("sheet") {
  69. name := sn.attr("name")
  70. rid := ""
  71. for _, a := range sn.Attrs {
  72. if a.Name.Local == "id" && strings.HasPrefix(a.Value, "rId") {
  73. rid = a.Value
  74. }
  75. }
  76. target, ok2 := rels[rid]
  77. if !ok2 {
  78. continue
  79. }
  80. partPath := resolvePartPath("xl", target)
  81. raw, ok3 := files[partPath]
  82. if !ok3 {
  83. continue
  84. }
  85. tree, err := parseXMLTree(raw)
  86. if err != nil {
  87. continue
  88. }
  89. ws := parseWorksheet(tree, shared, styleMap)
  90. ws.Name = name
  91. parseSheetDrawing(files, partPath, tree, ws, &chartIDSeq)
  92. parseSheetComments(files, partPath, ws)
  93. wb.Sheets = append(wb.Sheets, ws)
  94. }
  95. if len(wb.Sheets) == 0 {
  96. return nil, errors.New("no readable worksheets found in xlsx")
  97. }
  98. if wb.Active < 0 || wb.Active >= len(wb.Sheets) {
  99. wb.Active = 0
  100. }
  101. return wb, nil
  102. }
  103. /* ---------- shared strings ---------- */
  104. func parseSharedStrings(data []byte) []string {
  105. if data == nil {
  106. return nil
  107. }
  108. tree, err := parseXMLTree(data)
  109. if err != nil {
  110. return nil
  111. }
  112. var out []string
  113. for _, si := range tree.all("si") {
  114. var texts []string
  115. collectText(si, &texts)
  116. out = append(out, strings.Join(texts, ""))
  117. }
  118. return out
  119. }
  120. /* ---------- styles ---------- */
  121. type xlsxXfInfo struct {
  122. style *CellStyle // nil = plain
  123. }
  124. // parseXlsxStyles maps every cellXfs index to a webapp CellStyle
  125. func parseXlsxStyles(data []byte) []xlsxXfInfo {
  126. if data == nil {
  127. return nil
  128. }
  129. tree, err := parseXMLTree(data)
  130. if err != nil {
  131. return nil
  132. }
  133. // custom number format codes
  134. numCodes := map[int]string{}
  135. if nf := tree.first("numFmts"); nf != nil {
  136. for _, n := range nf.all("numFmt") {
  137. if id, err := strconv.Atoi(n.attr("numFmtId")); err == nil {
  138. numCodes[id] = n.attr("formatCode")
  139. }
  140. }
  141. }
  142. type fontInfo struct {
  143. b, i, u bool
  144. color string
  145. sizePx float64
  146. }
  147. var fonts []fontInfo
  148. if fs := tree.first("fonts"); fs != nil {
  149. for _, f := range fs.all("font") {
  150. fi := fontInfo{}
  151. if f.first("b") != nil {
  152. fi.b = true
  153. }
  154. if f.first("i") != nil {
  155. fi.i = true
  156. }
  157. if f.first("u") != nil {
  158. fi.u = true
  159. }
  160. if c := f.first("color"); c != nil {
  161. if rgb := c.attr("rgb"); len(rgb) == 8 {
  162. fi.color = "#" + strings.ToLower(rgb[2:])
  163. }
  164. }
  165. if sz := f.first("sz"); sz != nil {
  166. if v, err := strconv.ParseFloat(sz.attr("val"), 64); err == nil {
  167. fi.sizePx = v * 96.0 / 72.0
  168. }
  169. }
  170. fonts = append(fonts, fi)
  171. }
  172. }
  173. var fills []string
  174. if fl := tree.first("fills"); fl != nil {
  175. for _, f := range fl.all("fill") {
  176. bg := ""
  177. if pf := f.first("patternFill"); pf != nil && pf.attr("patternType") == "solid" {
  178. if fg := pf.first("fgColor"); fg != nil {
  179. if rgb := fg.attr("rgb"); len(rgb) == 8 {
  180. bg = "#" + strings.ToLower(rgb[2:])
  181. }
  182. }
  183. }
  184. fills = append(fills, bg)
  185. }
  186. }
  187. var out []xlsxXfInfo
  188. if cx := tree.first("cellXfs"); cx != nil {
  189. for _, xf := range cx.all("xf") {
  190. st := &CellStyle{}
  191. any := false
  192. if fid, err := strconv.Atoi(xf.attr("fontId")); err == nil && fid >= 0 && fid < len(fonts) {
  193. fi := fonts[fid]
  194. if fi.b {
  195. st.B = true
  196. any = true
  197. }
  198. if fi.i {
  199. st.I = true
  200. any = true
  201. }
  202. if fi.u {
  203. st.U = true
  204. any = true
  205. }
  206. if fi.color != "" && fi.color != "#000000" {
  207. st.Fc = fi.color
  208. any = true
  209. }
  210. if fi.sizePx > 0 && (fi.sizePx < 14 || fi.sizePx > 15.5) { // != default 11pt
  211. st.Fs = fi.sizePx
  212. any = true
  213. }
  214. }
  215. if flid, err := strconv.Atoi(xf.attr("fillId")); err == nil && flid >= 0 && flid < len(fills) {
  216. if fills[flid] != "" {
  217. st.Bg = fills[flid]
  218. any = true
  219. }
  220. }
  221. if bid, err := strconv.Atoi(xf.attr("borderId")); err == nil && bid > 0 {
  222. st.Bd = 1
  223. any = true
  224. }
  225. if al := xf.first("alignment"); al != nil {
  226. switch al.attr("horizontal") {
  227. case "left":
  228. st.Al = "l"
  229. any = true
  230. case "center":
  231. st.Al = "c"
  232. any = true
  233. case "right":
  234. st.Al = "r"
  235. any = true
  236. }
  237. if al.attr("wrapText") == "1" || al.attr("wrapText") == "true" {
  238. st.Wrap = true
  239. any = true
  240. }
  241. }
  242. if nid, err := strconv.Atoi(xf.attr("numFmtId")); err == nil && nid > 0 {
  243. fmtName, dec := numFmtIDToName(nid, numCodes)
  244. if fmtName != "" {
  245. st.Fmt = fmtName
  246. if dec >= 0 {
  247. d := dec
  248. st.Dec = &d
  249. }
  250. any = true
  251. }
  252. }
  253. if any {
  254. out = append(out, xlsxXfInfo{style: st})
  255. } else {
  256. out = append(out, xlsxXfInfo{})
  257. }
  258. }
  259. }
  260. return out
  261. }
  262. // numFmtIDToName maps builtin/custom number format ids to webapp fmt names
  263. func numFmtIDToName(id int, custom map[int]string) (string, int) {
  264. switch {
  265. case id >= 1 && id <= 2:
  266. return "number", decimalsInCode("0.00")
  267. case id == 3:
  268. return "number", 0
  269. case id == 4:
  270. return "number", 2
  271. case id == 9:
  272. return "percent", 0
  273. case id == 10:
  274. return "percent", 2
  275. case id >= 14 && id <= 17 || id == 22:
  276. return "date", -1
  277. case id == 44 || id == 5 || id == 6 || id == 7 || id == 8 || id == 42:
  278. return "currency", 2
  279. case id == 49:
  280. return "text", -1
  281. }
  282. code, ok := custom[id]
  283. if !ok {
  284. return "", -1
  285. }
  286. lc := strings.ToLower(code)
  287. switch {
  288. case strings.Contains(lc, "%"):
  289. return "percent", decimalsInCode(code)
  290. case strings.Contains(code, "$") || strings.Contains(code, "¤"):
  291. return "currency", decimalsInCode(code)
  292. case strings.Contains(lc, "yy") || strings.Contains(lc, "dd") ||
  293. (strings.Contains(lc, "mm") && !strings.Contains(lc, "0")):
  294. return "date", -1
  295. case code == "@":
  296. return "text", -1
  297. case strings.Contains(code, "0"):
  298. return "number", decimalsInCode(code)
  299. }
  300. return "", -1
  301. }
  302. func decimalsInCode(code string) int {
  303. i := strings.Index(code, ".")
  304. if i < 0 {
  305. return 0
  306. }
  307. n := 0
  308. for j := i + 1; j < len(code) && code[j] == '0'; j++ {
  309. n++
  310. }
  311. return n
  312. }
  313. /* ---------- worksheet ---------- */
  314. func parseWorksheet(tree *xnode, shared []string, styleMap []xlsxXfInfo) *WorkSheet {
  315. ws := &WorkSheet{
  316. Cells: map[string]*WorkCell{},
  317. ColW: map[string]float64{},
  318. RowH: map[string]float64{},
  319. }
  320. // frozen panes
  321. if pane := tree.path("sheetViews", "sheetView", "pane"); pane != nil && pane.attr("state") == "frozen" {
  322. fz := &FreezePane{}
  323. if x, err := strconv.Atoi(pane.attr("xSplit")); err == nil {
  324. fz.C = x
  325. }
  326. if y, err := strconv.Atoi(pane.attr("ySplit")); err == nil {
  327. fz.R = y
  328. }
  329. if fz.R > 0 || fz.C > 0 {
  330. ws.Freeze = fz
  331. }
  332. }
  333. // column widths
  334. if cols := tree.first("cols"); cols != nil {
  335. for _, c := range cols.all("col") {
  336. min, e1 := strconv.Atoi(c.attr("min"))
  337. max, e2 := strconv.Atoi(c.attr("max"))
  338. w, e3 := strconv.ParseFloat(c.attr("width"), 64)
  339. if e1 != nil || e2 != nil || e3 != nil {
  340. continue
  341. }
  342. if max-min > 64 {
  343. max = min + 64 // ignore column-range floods
  344. }
  345. for i := min; i <= max; i++ {
  346. ws.ColW[strconv.Itoa(i-1)] = float64(int(colCharsToPx(w)))
  347. }
  348. }
  349. }
  350. maxCol, maxRow := 0, 0
  351. if sd := tree.first("sheetData"); sd != nil {
  352. for _, row := range sd.all("row") {
  353. rIdx, err := strconv.Atoi(row.attr("r"))
  354. if err != nil {
  355. continue
  356. }
  357. if ht, err := strconv.ParseFloat(row.attr("ht"), 64); err == nil && row.attr("customHeight") == "1" {
  358. ws.RowH[strconv.Itoa(rIdx-1)] = float64(int(rowPtToPx(ht)))
  359. }
  360. for _, c := range row.all("c") {
  361. ref := c.attr("r")
  362. col, rw, ok := parseCellRef(ref)
  363. if !ok {
  364. continue
  365. }
  366. cell := parseXlsxCell(c, shared)
  367. var st *CellStyle
  368. if sIdx, err := strconv.Atoi(c.attr("s")); err == nil && sIdx >= 0 && sIdx < len(styleMap) {
  369. st = styleMap[sIdx].style
  370. }
  371. if cell == "" && st == nil {
  372. continue
  373. }
  374. wc := &WorkCell{V: cell}
  375. if st != nil {
  376. cp := *st
  377. wc.S = &cp
  378. }
  379. ws.Cells[cellRef(col, rw)] = wc
  380. if col > maxCol {
  381. maxCol = col
  382. }
  383. if rw > maxRow {
  384. maxRow = rw
  385. }
  386. }
  387. }
  388. }
  389. ws.Cols = maxCol + 6
  390. if ws.Cols < 26 {
  391. ws.Cols = 26
  392. }
  393. ws.Rows = maxRow + 21
  394. if ws.Rows < 200 {
  395. ws.Rows = 200
  396. }
  397. // merges
  398. if mc := tree.first("mergeCells"); mc != nil {
  399. for _, m := range mc.all("mergeCell") {
  400. if ref := m.attr("ref"); ref != "" {
  401. ws.Merges = append(ws.Merges, ref)
  402. }
  403. }
  404. }
  405. return ws
  406. }
  407. // parseXlsxCell extracts the raw editor value from a <c> element
  408. func parseXlsxCell(c *xnode, shared []string) string {
  409. // formulas win: the webapp recalculates them
  410. if f := c.first("f"); f != nil && strings.TrimSpace(f.Text) != "" {
  411. return "=" + f.Text
  412. }
  413. t := c.attr("t")
  414. switch t {
  415. case "inlineStr":
  416. if is := c.first("is"); is != nil {
  417. var texts []string
  418. collectText(is, &texts)
  419. return textAsRaw(strings.Join(texts, ""))
  420. }
  421. return ""
  422. case "s":
  423. if v := c.first("v"); v != nil {
  424. if idx, err := strconv.Atoi(strings.TrimSpace(v.Text)); err == nil && idx >= 0 && idx < len(shared) {
  425. return textAsRaw(shared[idx])
  426. }
  427. }
  428. return ""
  429. case "b":
  430. if v := c.first("v"); v != nil {
  431. if strings.TrimSpace(v.Text) == "1" {
  432. return "TRUE"
  433. }
  434. return "FALSE"
  435. }
  436. return ""
  437. case "str":
  438. if v := c.first("v"); v != nil {
  439. return textAsRaw(v.Text)
  440. }
  441. return ""
  442. default: // "n" or absent = number
  443. if v := c.first("v"); v != nil {
  444. return strings.TrimSpace(v.Text)
  445. }
  446. return ""
  447. }
  448. }
  449. // textAsRaw keeps string-typed values as strings in the editor: text that
  450. // would re-parse as a number/bool gets Excel's leading-quote escape
  451. func textAsRaw(s string) string {
  452. t := strings.TrimSpace(s)
  453. if t == "" {
  454. return s
  455. }
  456. up := strings.ToUpper(t)
  457. if looksNumeric(t) || up == "TRUE" || up == "FALSE" || strings.HasPrefix(t, "=") || strings.HasPrefix(t, "'") {
  458. return "'" + s
  459. }
  460. return s
  461. }