odp_reader.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. package office
  2. /*
  3. odp_reader.go - Parse an OpenDocument Presentation (.odp) into a
  4. Presentation, scaled into the 960x540 editor space.
  5. Frames with text boxes become text objects (paragraph lines joined with
  6. <br>), draw:image frames become image objects (pictures re-inlined as
  7. data URLs), draw:rect / draw:ellipse become shapes, draw:line lines and
  8. framed tables become table objects. Slide backgrounds and speaker
  9. notes are kept.
  10. */
  11. import (
  12. "encoding/base64"
  13. "errors"
  14. "fmt"
  15. "strings"
  16. )
  17. // ParseOdp converts raw .odp bytes into a Presentation
  18. func ParseOdp(data []byte) (*Presentation, error) {
  19. files, mime, err := readOdfZip(data)
  20. if err != nil {
  21. return nil, err
  22. }
  23. if mime != "" && mime != odpMime {
  24. return nil, errors.New("not an OpenDocument presentation (mimetype " + mime + ")")
  25. }
  26. content, ok := files["content.xml"]
  27. if !ok {
  28. return nil, errors.New("odp is missing content.xml")
  29. }
  30. tree, err := parseOdfXML(content)
  31. if err != nil {
  32. return nil, errors.New("cannot parse content.xml: " + err.Error())
  33. }
  34. root := tree.first("document-content")
  35. if root == nil {
  36. return nil, errors.New("content.xml has no document-content root")
  37. }
  38. // source page size -> scale into the 960x540 editor space
  39. scaleX, scaleY := 1.0, 1.0
  40. if raw, ok := files["styles.xml"]; ok {
  41. if st, err := parseOdfXML(raw); err == nil {
  42. if pp := st.path("document-styles", "automatic-styles", "page-layout", "page-layout-properties"); pp != nil {
  43. if wPx := odfLenToPx(pp.attr("page-width")); wPx > 0 {
  44. scaleX = float64(slidePxW) / wPx
  45. }
  46. if hPx := odfLenToPx(pp.attr("page-height")); hPx > 0 {
  47. scaleY = float64(slidePxH) / hPx
  48. }
  49. }
  50. }
  51. }
  52. pageBg := map[string]string{}
  53. shapeStyle := map[string]*onode{}
  54. if auto := root.first("automatic-styles"); auto != nil {
  55. for _, st := range auto.all("style") {
  56. name := st.attr("name")
  57. if name == "" {
  58. continue
  59. }
  60. switch st.attr("family") {
  61. case "drawing-page":
  62. if dp := st.first("drawing-page-properties"); dp != nil {
  63. if c := dp.attr("fill-color"); strings.HasPrefix(c, "#") {
  64. pageBg[name] = strings.ToLower(c)
  65. }
  66. }
  67. case "graphic", "presentation":
  68. shapeStyle[name] = st
  69. }
  70. }
  71. }
  72. pres := root.path("body", "presentation")
  73. if pres == nil {
  74. return nil, errors.New("odp has no presentation body")
  75. }
  76. out := &Presentation{Size: []int{slidePxW, slidePxH}, Slides: []*Slide{}}
  77. cv := &odpConverter{files: files, sx: scaleX, sy: scaleY, shapeStyle: shapeStyle}
  78. for pi, page := range pres.all("page") {
  79. slide := &Slide{ID: fmt.Sprintf("s-odp-%d", pi+1), Objects: []*Object{}}
  80. if bg, ok := pageBg[page.attr("style-name")]; ok {
  81. slide.Bg = bg
  82. }
  83. for _, c := range page.children {
  84. if c.el == nil {
  85. continue
  86. }
  87. switch c.el.name {
  88. case "frame":
  89. cv.frame(c.el, slide)
  90. case "rect", "ellipse", "custom-shape":
  91. cv.shape(c.el, slide)
  92. case "line":
  93. cv.line(c.el, slide)
  94. case "notes":
  95. slide.Notes = strings.TrimSpace(c.el.allText())
  96. }
  97. }
  98. out.Slides = append(out.Slides, slide)
  99. }
  100. if len(out.Slides) == 0 {
  101. return nil, errors.New("no slides found in odp")
  102. }
  103. return out, nil
  104. }
  105. type odpConverter struct {
  106. files map[string][]byte
  107. sx, sy float64
  108. shapeStyle map[string]*onode
  109. seq int
  110. }
  111. func (cv *odpConverter) geom(n *onode) (x, y, w, h float64) {
  112. return odfLenToPx(n.attr("x")) * cv.sx, odfLenToPx(n.attr("y")) * cv.sy,
  113. odfLenToPx(n.attr("width")) * cv.sx, odfLenToPx(n.attr("height")) * cv.sy
  114. }
  115. func (cv *odpConverter) nextID() string {
  116. cv.seq++
  117. return fmt.Sprintf("o-odp-%d", cv.seq)
  118. }
  119. func (cv *odpConverter) frame(n *onode, slide *Slide) {
  120. x, y, w, h := cv.geom(n)
  121. if w <= 0 || h <= 0 {
  122. w, h = 200, 100
  123. }
  124. if img := n.first("image"); img != nil {
  125. raw, ok := cv.files[strings.TrimPrefix(img.attr("href"), "./")]
  126. if !ok {
  127. return
  128. }
  129. ext := strings.TrimPrefix(strings.ToLower(pathExtOf(img.attr("href"))), ".")
  130. if ext == "jpg" {
  131. ext = "jpeg"
  132. }
  133. if ext != "png" && ext != "jpeg" && ext != "gif" {
  134. return
  135. }
  136. slide.Objects = append(slide.Objects, &Object{
  137. ID: cv.nextID(), Type: "image", X: x, Y: y, W: w, H: h,
  138. Z: len(slide.Objects) + 1,
  139. Props: Props{Src: "data:image/" + ext + ";base64," +
  140. base64.StdEncoding.EncodeToString(raw), Fit: "contain"},
  141. })
  142. return
  143. }
  144. if tbl := n.first("table"); tbl != nil {
  145. cv.table(tbl, slide, x, y, w, h)
  146. return
  147. }
  148. if tb := n.first("text-box"); tb != nil {
  149. var lines []string
  150. for _, p := range tb.all("p") {
  151. lines = append(lines, p.allText())
  152. }
  153. if len(lines) == 0 {
  154. return
  155. }
  156. slide.Objects = append(slide.Objects, &Object{
  157. ID: cv.nextID(), Type: "text", X: x, Y: y, W: w, H: h,
  158. Z: len(slide.Objects) + 1,
  159. Props: Props{HTML: linesToHTML(lines), FontSize: 24, Align: "left"},
  160. })
  161. }
  162. }
  163. func (cv *odpConverter) styleFill(n *onode) (fill, stroke string, strokeW float64) {
  164. st, ok := cv.shapeStyle[n.attr("style-name")]
  165. if !ok {
  166. return "", "", 0
  167. }
  168. gp := st.first("graphic-properties")
  169. if gp == nil {
  170. return "", "", 0
  171. }
  172. if c := gp.attr("fill-color"); strings.HasPrefix(c, "#") {
  173. fill = strings.ToLower(c)
  174. }
  175. if c := gp.attr("stroke-color"); strings.HasPrefix(c, "#") {
  176. stroke = strings.ToLower(c)
  177. }
  178. strokeW = odfLenToPx(gp.attr("stroke-width"))
  179. return fill, stroke, strokeW
  180. }
  181. func (cv *odpConverter) shape(n *onode, slide *Slide) {
  182. x, y, w, h := cv.geom(n)
  183. if w <= 0 || h <= 0 {
  184. return
  185. }
  186. kind := "rect"
  187. if n.name == "ellipse" {
  188. kind = "ellipse"
  189. } else if n.attr("corner-radius") != "" {
  190. kind = "round"
  191. }
  192. fill, stroke, sw := cv.styleFill(n)
  193. if fill == "" {
  194. fill = "#e07b1f"
  195. }
  196. slide.Objects = append(slide.Objects, &Object{
  197. ID: cv.nextID(), Type: "shape", X: x, Y: y, W: w, H: h,
  198. Z: len(slide.Objects) + 1,
  199. Props: Props{Kind: kind, Fill: fill, Stroke: stroke, StrokeW: sw,
  200. Text: strings.TrimSpace(n.allText())},
  201. })
  202. }
  203. func (cv *odpConverter) line(n *onode, slide *Slide) {
  204. x1 := odfLenToPx(n.attr("x1")) * cv.sx
  205. y1 := odfLenToPx(n.attr("y1")) * cv.sy
  206. x2 := odfLenToPx(n.attr("x2")) * cv.sx
  207. y2 := odfLenToPx(n.attr("y2")) * cv.sy
  208. _, stroke, sw := cv.styleFill(n)
  209. if stroke == "" {
  210. stroke = "#333333"
  211. }
  212. if sw <= 0 {
  213. sw = 2
  214. }
  215. slide.Objects = append(slide.Objects, &Object{
  216. ID: cv.nextID(), Type: "line", X: x1, Y: y1, W: x2 - x1, H: y2 - y1,
  217. Z: len(slide.Objects) + 1,
  218. Props: Props{Stroke: stroke, StrokeW: sw},
  219. })
  220. }
  221. func (cv *odpConverter) table(tbl *onode, slide *Slide, x, y, w, h float64) {
  222. var rows [][]string
  223. for _, tr := range tbl.all("table-row") {
  224. var row []string
  225. for _, c := range tr.children {
  226. if c.el == nil || c.el.name != "table-cell" {
  227. continue
  228. }
  229. row = append(row, xmlEscape(strings.TrimSpace(c.el.allText())))
  230. }
  231. if len(row) > 0 {
  232. rows = append(rows, row)
  233. }
  234. }
  235. if len(rows) == 0 {
  236. return
  237. }
  238. slide.Objects = append(slide.Objects, &Object{
  239. ID: cv.nextID(), Type: "table", X: x, Y: y, W: w, H: h,
  240. Z: len(slide.Objects) + 1,
  241. Props: Props{Rows: rows, FontSize: 16},
  242. })
  243. }