odp_reader.go 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245
  1. package odf
  2. /*
  3. odp_reader.go - Parse an OpenDocument Presentation (.odp) into a
  4. Presentation, scaled into the editor space (960 wide, the page's shape).
  5. ODF states appearance the same way PresentationML does: almost never on
  6. the element itself. A paragraph names a style, that style names a parent,
  7. the parent lives in styles.xml, and a frame with presentation:class="title"
  8. takes its position and its typography from the matching placeholder on the
  9. master page. Reading only what a shape spells out - which is what the
  10. first version of this reader did - gives every text box the same 24px
  11. default and drops the deck's whole design.
  12. So the reader resolves, lowest priority first:
  13. styles.xml office:styles the named style and its parents
  14. styles.xml automatic-styles the master page's own styles
  15. content.xml automatic-styles the per-document generated styles
  16. the paragraph's style, then the span's
  17. and draws each page on top of its master page's decoration, the way an
  18. ODF viewer does.
  19. */
  20. import (
  21. "encoding/base64"
  22. "errors"
  23. "fmt"
  24. "math"
  25. "strconv"
  26. "strings"
  27. "imuslab.com/arozos/mod/office/internal/fonts"
  28. "imuslab.com/arozos/mod/office/internal/slidetext"
  29. "imuslab.com/arozos/mod/office/internal/units"
  30. "imuslab.com/arozos/mod/office/internal/xmltree"
  31. "imuslab.com/arozos/mod/office/metafile"
  32. "imuslab.com/arozos/mod/office/model"
  33. )
  34. // odpStyle is a resolved ODF style: the graphic, paragraph and text
  35. // properties that matter to the editor, folded down the parent chain
  36. type odpStyle struct {
  37. // text
  38. SizePt float64
  39. Font string
  40. Bold bool
  41. Italic bool
  42. Underline bool
  43. Strike bool
  44. Color string
  45. Highlight string
  46. // paragraph
  47. Align string
  48. LineHeight float64 // unitless
  49. SpaceTop float64 // px
  50. SpaceBot float64 // px
  51. MarginLeft float64 // px
  52. Indent float64 // px
  53. // graphic
  54. Fill string // "" unset, "none", or "#rrggbb"
  55. Stroke string
  56. StrokeW float64
  57. Dash bool
  58. VAlign string // top | middle | bottom
  59. PadT float64
  60. PadR float64
  61. PadB float64
  62. PadL float64
  63. HasPad bool
  64. AutoGrow bool
  65. Rotation float64
  66. ArrowEnd bool
  67. ArrowHead bool
  68. }
  69. var odpDefaultStyle = odpStyle{SizePt: 18, Align: "left", Color: "#000000"}
  70. // ParseOdp converts raw .odp bytes into a Presentation
  71. func ParseOdp(data []byte) (*model.Presentation, error) {
  72. files, mime, err := readOdfZip(data)
  73. if err != nil {
  74. return nil, err
  75. }
  76. if mime != "" && mime != odpMime {
  77. return nil, errors.New("not an OpenDocument presentation (mimetype " + mime + ")")
  78. }
  79. content, ok := files["content.xml"]
  80. if !ok {
  81. return nil, errors.New("odp is missing content.xml")
  82. }
  83. tree, err := parseOdfXML(content)
  84. if err != nil {
  85. return nil, errors.New("cannot parse content.xml: " + err.Error())
  86. }
  87. root := tree.first("document-content")
  88. if root == nil {
  89. return nil, errors.New("content.xml has no document-content root")
  90. }
  91. cv := &odpConverter{
  92. files: files,
  93. sx: 1,
  94. sy: 1,
  95. styles: map[string]*onode{},
  96. lists: map[string]*onode{},
  97. pageBg: map[string]string{},
  98. }
  99. pageW, pageH := model.SlidePxW, model.SlidePxH
  100. var stylesRoot *onode
  101. if raw, ok := files["styles.xml"]; ok {
  102. if st, err := parseOdfXML(raw); err == nil {
  103. stylesRoot = st.first("document-styles")
  104. }
  105. }
  106. if stylesRoot != nil {
  107. // source page size -> the editor's: 960 wide, the page's shape
  108. for _, auto := range []*onode{stylesRoot.first("automatic-styles")} {
  109. if auto == nil {
  110. continue
  111. }
  112. for _, pl := range auto.all("page-layout") {
  113. pp := pl.first("page-layout-properties")
  114. if pp == nil {
  115. continue
  116. }
  117. wPx := odfLenToPx(pp.attr("page-width"))
  118. hPx := odfLenToPx(pp.attr("page-height"))
  119. if wPx > 0 && hPx > 0 {
  120. pageW, pageH = model.SlideSizeFor(wPx, hPx)
  121. cv.sx = float64(pageW) / wPx
  122. cv.sy = float64(pageH) / hPx
  123. }
  124. }
  125. }
  126. cv.collectStyles(stylesRoot.first("styles"))
  127. cv.collectStyles(stylesRoot.first("automatic-styles"))
  128. if ms := stylesRoot.first("master-styles"); ms != nil {
  129. cv.masters = map[string]*onode{}
  130. for _, mp := range ms.all("master-page") {
  131. cv.masters[mp.attr("name")] = mp
  132. }
  133. }
  134. }
  135. cv.collectStyles(root.first("automatic-styles"))
  136. pres := root.path("body", "presentation")
  137. if pres == nil {
  138. return nil, errors.New("odp has no presentation body")
  139. }
  140. out := &model.Presentation{Size: []int{pageW, pageH}, Slides: []*model.Slide{}}
  141. for pi, page := range pres.all("page") {
  142. slide := &model.Slide{ID: fmt.Sprintf("s-odp-%d", pi+1), Objects: []*model.Object{}}
  143. master := cv.masterOf(page)
  144. slide.Bg = cv.backgroundOf(page, master)
  145. cv.master = master
  146. // the master page's own drawing sits underneath the slide's
  147. if master != nil {
  148. cv.walkPage(master, slide, true)
  149. }
  150. cv.walkPage(page, slide, false)
  151. for _, c := range page.children {
  152. if c.el != nil && c.el.name == "notes" {
  153. slide.Notes = strings.TrimSpace(c.el.allText())
  154. }
  155. }
  156. out.Slides = append(out.Slides, slide)
  157. }
  158. if len(out.Slides) == 0 {
  159. return nil, errors.New("no slides found in odp")
  160. }
  161. return out, nil
  162. }
  163. type odpConverter struct {
  164. files map[string][]byte
  165. sx, sy float64
  166. styles map[string]*onode
  167. lists map[string]*onode
  168. masters map[string]*onode
  169. master *onode
  170. pageBg map[string]string
  171. seq int
  172. // runs of the text body being built (see buildText)
  173. runs *odpRunFlags
  174. }
  175. // odpRunFlags gathers whether every run of a text body is bold / italic /
  176. // underlined - the only case in which the box itself may say so
  177. type odpRunFlags struct {
  178. any, bold, italic, under bool
  179. }
  180. // collectStyles indexes every style:style and text:list-style in a container
  181. func (cv *odpConverter) collectStyles(container *onode) {
  182. if container == nil {
  183. return
  184. }
  185. for _, st := range container.all("style") {
  186. if n := st.attr("name"); n != "" {
  187. cv.styles[n] = st
  188. }
  189. }
  190. for _, st := range container.all("list-style") {
  191. if n := st.attr("name"); n != "" {
  192. cv.lists[n] = st
  193. }
  194. }
  195. for _, st := range container.all("default-style") {
  196. if f := st.attr("family"); f != "" {
  197. cv.styles["#default-"+f] = st
  198. }
  199. }
  200. for _, dp := range container.all("style") {
  201. if dp.attr("family") != "drawing-page" {
  202. continue
  203. }
  204. if props := dp.first("drawing-page-properties"); props != nil {
  205. if c := props.attr("fill-color"); strings.HasPrefix(c, "#") {
  206. cv.pageBg[dp.attr("name")] = strings.ToLower(c)
  207. }
  208. }
  209. }
  210. }
  211. func (cv *odpConverter) masterOf(page *onode) *onode {
  212. if cv.masters == nil {
  213. return nil
  214. }
  215. return cv.masters[page.attr("master-page-name")]
  216. }
  217. // backgroundOf resolves a page's fill, falling back to its master's
  218. func (cv *odpConverter) backgroundOf(page, master *onode) string {
  219. if bg, ok := cv.pageBg[page.attr("style-name")]; ok {
  220. return bg
  221. }
  222. if master != nil {
  223. if bg, ok := cv.pageBg[master.attr("style-name")]; ok {
  224. return bg
  225. }
  226. if bg, ok := cv.pageBg[master.attr("page-layout-name")]; ok {
  227. return bg
  228. }
  229. }
  230. return ""
  231. }
  232. // walkPage draws every shape on a page. When masterOnly is set the
  233. // placeholders are skipped: on a master page they are prototypes that
  234. // state where a slide's title and body go, not content of their own.
  235. func (cv *odpConverter) walkPage(page *onode, slide *model.Slide, masterOnly bool) {
  236. for _, c := range page.children {
  237. if c.el == nil {
  238. continue
  239. }
  240. if masterOnly && c.el.attr("class") != "" {
  241. continue
  242. }
  243. cv.drawShape(c.el, slide, 0, 0, 1, 1)
  244. }
  245. }
  246. // drawShape converts one drawing element, recursing into groups. ox/oy and
  247. // kx/ky carry the transform a draw:g imposes on its children.
  248. func (cv *odpConverter) drawShape(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
  249. switch n.name {
  250. case "frame":
  251. cv.frame(n, slide, ox, oy, kx, ky)
  252. case "rect", "ellipse", "circle", "custom-shape", "polygon", "regular-polygon":
  253. cv.shape(n, slide, ox, oy, kx, ky)
  254. case "line", "connector":
  255. cv.line(n, slide, ox, oy, kx, ky)
  256. case "g":
  257. // a group has no transform of its own in ODF - its children carry
  258. // absolute coordinates - so it is simply flattened
  259. for _, c := range n.children {
  260. if c.el != nil {
  261. cv.drawShape(c.el, slide, ox, oy, kx, ky)
  262. }
  263. }
  264. }
  265. }
  266. /* ---------------- style resolution ---------------- */
  267. // resolveStyle folds a style and its parent chain into one odpStyle
  268. func (cv *odpConverter) resolveStyle(name string, base odpStyle) odpStyle {
  269. if name == "" {
  270. return base
  271. }
  272. // walk to the root of the parent chain first, then apply downwards so
  273. // the most specific style wins
  274. var chain []*onode
  275. seen := map[string]bool{}
  276. for cur := name; cur != "" && !seen[cur]; {
  277. seen[cur] = true
  278. st, ok := cv.styles[cur]
  279. if !ok {
  280. break
  281. }
  282. chain = append([]*onode{st}, chain...)
  283. cur = st.attr("parent-style-name")
  284. }
  285. out := base
  286. for _, st := range chain {
  287. cv.applyStyleNode(&out, st)
  288. }
  289. return out
  290. }
  291. // applyStyleNode folds one style:style element's properties into dst
  292. func (cv *odpConverter) applyStyleNode(dst *odpStyle, st *onode) {
  293. if tp := st.first("text-properties"); tp != nil {
  294. applyOdfTextProps(dst, tp)
  295. }
  296. if pp := st.first("paragraph-properties"); pp != nil {
  297. cv.applyOdfParaProps(dst, pp)
  298. }
  299. if gp := st.first("graphic-properties"); gp != nil {
  300. cv.applyOdfGraphicProps(dst, gp)
  301. }
  302. }
  303. func applyOdfTextProps(dst *odpStyle, tp *onode) {
  304. if v := tp.attr("font-size"); v != "" {
  305. if pt := odfFontSizePt(v, dst.SizePt); pt > 0 {
  306. dst.SizePt = pt
  307. }
  308. }
  309. if v := tp.attr("font-name"); v != "" {
  310. dst.Font = v
  311. }
  312. if v := tp.attr("font-family"); v != "" {
  313. dst.Font = strings.Trim(v, "'\"")
  314. }
  315. if v := tp.attr("font-weight"); v != "" {
  316. dst.Bold = v == "bold" || units.ParseNum(v) >= 600
  317. }
  318. if v := tp.attr("font-style"); v != "" {
  319. dst.Italic = v == "italic" || v == "oblique"
  320. }
  321. if v := tp.attr("text-underline-style"); v != "" {
  322. dst.Underline = v != "none"
  323. }
  324. if v := tp.attr("text-line-through-style"); v != "" {
  325. dst.Strike = v != "none"
  326. }
  327. if v := tp.attr("color"); strings.HasPrefix(v, "#") {
  328. dst.Color = strings.ToLower(v)
  329. }
  330. if v := tp.attr("background-color"); strings.HasPrefix(v, "#") {
  331. dst.Highlight = strings.ToLower(v)
  332. }
  333. }
  334. func (cv *odpConverter) applyOdfParaProps(dst *odpStyle, pp *onode) {
  335. switch pp.attr("text-align") {
  336. case "center":
  337. dst.Align = "center"
  338. case "end", "right":
  339. dst.Align = "right"
  340. case "justify":
  341. dst.Align = "justify"
  342. case "start", "left":
  343. dst.Align = "left"
  344. }
  345. if v := pp.attr("line-height"); v != "" {
  346. if strings.HasSuffix(v, "%") {
  347. dst.LineHeight = units.ParseNum(v) / 100 * model.LineHeightFactor
  348. } else if px := odfLenToPx(v); px > 0 && dst.SizePt > 0 {
  349. dst.LineHeight = px / units.PtToPx(dst.SizePt)
  350. }
  351. }
  352. if v := pp.attr("margin-top"); v != "" {
  353. dst.SpaceTop = odfLenToPx(v) * cv.sy
  354. }
  355. if v := pp.attr("margin-bottom"); v != "" {
  356. dst.SpaceBot = odfLenToPx(v) * cv.sy
  357. }
  358. if v := pp.attr("margin-left"); v != "" {
  359. dst.MarginLeft = odfLenToPx(v) * cv.sx
  360. }
  361. if v := pp.attr("text-indent"); v != "" {
  362. dst.Indent = odfLenToPx(v) * cv.sx
  363. }
  364. }
  365. func (cv *odpConverter) applyOdfGraphicProps(dst *odpStyle, gp *onode) {
  366. switch gp.attr("fill") {
  367. case "none":
  368. dst.Fill = "none"
  369. case "solid", "bitmap", "gradient", "hatch":
  370. if c := gp.attr("fill-color"); strings.HasPrefix(c, "#") {
  371. dst.Fill = strings.ToLower(c)
  372. }
  373. default:
  374. if c := gp.attr("fill-color"); strings.HasPrefix(c, "#") {
  375. dst.Fill = strings.ToLower(c)
  376. }
  377. }
  378. switch gp.attr("stroke") {
  379. case "none":
  380. dst.Stroke = "none"
  381. dst.StrokeW = 0
  382. case "dash":
  383. dst.Dash = true
  384. fallthrough
  385. case "solid":
  386. if c := gp.attr("stroke-color"); strings.HasPrefix(c, "#") {
  387. dst.Stroke = strings.ToLower(c)
  388. } else if dst.Stroke == "" || dst.Stroke == "none" {
  389. dst.Stroke = "#000000"
  390. }
  391. if w := odfLenToPx(gp.attr("stroke-width")) * cv.sx; w > 0 {
  392. dst.StrokeW = w
  393. } else if dst.StrokeW <= 0 {
  394. dst.StrokeW = 1
  395. }
  396. }
  397. switch gp.attr("textarea-vertical-align") {
  398. case "middle":
  399. dst.VAlign = "middle"
  400. case "bottom":
  401. dst.VAlign = "bottom"
  402. case "top":
  403. dst.VAlign = "top"
  404. }
  405. for attr, field := range map[string]*float64{
  406. "padding-top": &dst.PadT, "padding-right": &dst.PadR,
  407. "padding-bottom": &dst.PadB, "padding-left": &dst.PadL,
  408. } {
  409. if v := gp.attr(attr); v != "" {
  410. *field = odfLenToPx(v) * cv.sx
  411. dst.HasPad = true
  412. }
  413. }
  414. if v := gp.attr("padding"); v != "" {
  415. p := odfLenToPx(v) * cv.sx
  416. dst.PadT, dst.PadR, dst.PadB, dst.PadL = p, p, p, p
  417. dst.HasPad = true
  418. }
  419. if v := gp.attr("marker-end"); v != "" && v != "none" {
  420. dst.ArrowEnd = true
  421. }
  422. if v := gp.attr("marker-start"); v != "" && v != "none" {
  423. dst.ArrowHead = true
  424. }
  425. }
  426. // odfFontSizePt reads a font size, which may be relative ("120%")
  427. func odfFontSizePt(v string, current float64) float64 {
  428. if strings.HasSuffix(v, "%") {
  429. return current * units.ParseNum(v) / 100
  430. }
  431. if strings.HasSuffix(v, "pt") {
  432. return units.ParseNum(v)
  433. }
  434. if px := odfLenToPx(v); px > 0 {
  435. return px * 0.75
  436. }
  437. return 0
  438. }
  439. /* ---------------- geometry ---------------- */
  440. func (cv *odpConverter) geom(n *onode, ox, oy, kx, ky float64) (x, y, w, h float64) {
  441. x = ox + odfLenToPx(n.attr("x"))*cv.sx*kx
  442. y = oy + odfLenToPx(n.attr("y"))*cv.sy*ky
  443. w = odfLenToPx(n.attr("width")) * cv.sx * kx
  444. h = odfLenToPx(n.attr("height")) * cv.sy * ky
  445. return x, y, w, h
  446. }
  447. // odfRotation reads the rotation out of a draw:transform attribute, in
  448. // degrees clockwise (ODF states radians counter-clockwise)
  449. func odfRotation(n *onode) (deg float64, tx, ty float64) {
  450. t := n.attr("transform")
  451. if t == "" {
  452. return 0, 0, 0
  453. }
  454. if i := strings.Index(t, "rotate"); i >= 0 {
  455. if open := strings.Index(t[i:], "("); open >= 0 {
  456. rest := t[i+open+1:]
  457. if close := strings.Index(rest, ")"); close >= 0 {
  458. deg = -units.ParseNum(strings.TrimSpace(rest[:close])) * 180 / math.Pi
  459. }
  460. }
  461. }
  462. if i := strings.Index(t, "translate"); i >= 0 {
  463. if open := strings.Index(t[i:], "("); open >= 0 {
  464. rest := t[i+open+1:]
  465. if close := strings.Index(rest, ")"); close >= 0 {
  466. parts := strings.Fields(strings.TrimSpace(rest[:close]))
  467. if len(parts) == 2 {
  468. tx, ty = odfLenToPx(parts[0]), odfLenToPx(parts[1])
  469. }
  470. }
  471. }
  472. }
  473. return deg, tx, ty
  474. }
  475. func (cv *odpConverter) nextID() string {
  476. cv.seq++
  477. return fmt.Sprintf("o-odp-%d", cv.seq)
  478. }
  479. func (cv *odpConverter) add(slide *model.Slide, o *model.Object) {
  480. o.ID = cv.nextID()
  481. o.Z = len(slide.Objects) + 1
  482. slide.Objects = append(slide.Objects, o)
  483. }
  484. // styleOf resolves the drawing style of a shape, including the
  485. // presentation style a placeholder inherits
  486. func (cv *odpConverter) styleOf(n *onode) odpStyle {
  487. st := odpDefaultStyle
  488. st = cv.resolveStyle("#default-graphic", st)
  489. if ps := n.attr("class"); ps != "" && cv.master != nil {
  490. // a placeholder takes the master's matching frame's style
  491. if mf := cv.masterFrame(ps); mf != nil {
  492. st = cv.resolveStyle(mf.attr("style-name"), st)
  493. st = cv.resolveStyle(mf.attr("text-style-name"), st)
  494. }
  495. }
  496. st = cv.resolveStyle(n.attr("style-name"), st)
  497. st = cv.resolveStyle(n.attr("text-style-name"), st)
  498. return st
  499. }
  500. // masterFrame finds the placeholder frame of a given class on the master
  501. func (cv *odpConverter) masterFrame(class string) *onode {
  502. if cv.master == nil {
  503. return nil
  504. }
  505. for _, c := range cv.master.children {
  506. if c.el != nil && c.el.name == "frame" && c.el.attr("class") == class {
  507. return c.el
  508. }
  509. }
  510. return nil
  511. }
  512. /* ---------------- shapes ---------------- */
  513. func (cv *odpConverter) frame(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
  514. x, y, w, h := cv.geom(n, ox, oy, kx, ky)
  515. if (w <= 0 || h <= 0) && n.attr("class") != "" {
  516. // a placeholder with no geometry of its own takes the master's
  517. if mf := cv.masterFrame(n.attr("class")); mf != nil {
  518. x, y, w, h = cv.geom(mf, ox, oy, kx, ky)
  519. }
  520. }
  521. if w <= 0 || h <= 0 {
  522. return
  523. }
  524. rot, _, _ := odfRotation(n)
  525. st := cv.styleOf(n)
  526. if img := n.first("image"); img != nil {
  527. href := strings.TrimPrefix(img.attr("href"), "./")
  528. raw, ok := cv.files[href]
  529. if !ok {
  530. return
  531. }
  532. ext := strings.TrimPrefix(strings.ToLower(pathExtOf(href)), ".")
  533. if ext == "jpg" {
  534. ext = "jpeg"
  535. }
  536. if ext == "wmf" || ext == "emf" {
  537. if raw, ext, ok = metafile.BrowserPicture(raw, ext); !ok {
  538. return
  539. }
  540. }
  541. if ext != "png" && ext != "jpeg" && ext != "gif" {
  542. return
  543. }
  544. props := model.Props{Src: "data:image/" + ext + ";base64," +
  545. base64.StdEncoding.EncodeToString(raw), Fit: "fill"}
  546. if c := odfClipFractions(n.attr("clip")); c != nil {
  547. props.Crop = c
  548. }
  549. cv.add(slide, &model.Object{Type: "image", X: x, Y: y, W: w, H: h, Rot: rot, Props: props})
  550. return
  551. }
  552. if tbl := n.first("table"); tbl != nil {
  553. cv.table(tbl, slide, x, y, w, h, st)
  554. return
  555. }
  556. if tb := n.first("text-box"); tb != nil {
  557. res := cv.buildText(tb, st)
  558. if res.HTML == "" {
  559. return
  560. }
  561. cv.add(slide, &model.Object{Type: "text", X: x, Y: y, W: w, H: h, Rot: rot,
  562. Props: cv.textProps(res, st)})
  563. }
  564. }
  565. // textProps assembles the editor properties of a text body
  566. func (cv *odpConverter) textProps(res odpText, st odpStyle) model.Props {
  567. p := model.Props{
  568. HTML: res.HTML, FontSize: units.Round2(units.PtToPx(res.First.SizePt)),
  569. Color: res.First.Color, Align: res.First.Align,
  570. Bold: res.Bold, Italic: res.Italic,
  571. Underline: res.Underline,
  572. FontFamily: fonts.StackFor(res.First.Font, res.First.Font),
  573. VAlign: st.VAlign,
  574. LineHeight: units.Round2(odpLineHeight(res.First)),
  575. }
  576. if p.VAlign == "" {
  577. p.VAlign = "top"
  578. }
  579. if st.HasPad {
  580. p.Pad = []float64{units.Round2(st.PadT), units.Round2(st.PadR), units.Round2(st.PadB), units.Round2(st.PadL)}
  581. }
  582. return p
  583. }
  584. func odpLineHeight(st odpStyle) float64 {
  585. if st.LineHeight > 0 {
  586. return st.LineHeight
  587. }
  588. return model.LineHeightFactor
  589. }
  590. func (cv *odpConverter) shape(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
  591. x, y, w, h := cv.geom(n, ox, oy, kx, ky)
  592. if w <= 0 || h <= 0 {
  593. return
  594. }
  595. rot, _, _ := odfRotation(n)
  596. st := cv.styleOf(n)
  597. kind := "rect"
  598. switch n.name {
  599. case "ellipse", "circle":
  600. kind = "ellipse"
  601. case "polygon", "regular-polygon":
  602. kind = "diamond"
  603. case "custom-shape":
  604. kind = odpCustomShapeKind(n)
  605. default:
  606. if n.attr("corner-radius") != "" {
  607. kind = "roundRect"
  608. }
  609. }
  610. fill := st.Fill
  611. if fill == "" {
  612. fill = "#e07b1f"
  613. }
  614. stroke := st.Stroke
  615. if stroke == "none" {
  616. stroke = ""
  617. }
  618. res := cv.buildText(n, st)
  619. props := model.Props{Kind: kind, Fill: fill, Stroke: stroke, StrokeW: units.Round2(st.StrokeW),
  620. Dash: st.Dash, HTML: res.HTML, Text: res.Plain,
  621. TextColor: res.First.Color, FontSize: units.Round2(units.PtToPx(res.First.SizePt)),
  622. FontFamily: fonts.StackFor(res.First.Font, res.First.Font),
  623. Bold: res.Bold, Italic: res.Italic,
  624. Align: res.First.Align, VAlign: st.VAlign,
  625. LineHeight: units.Round2(odpLineHeight(res.First)),
  626. }
  627. if props.VAlign == "" {
  628. props.VAlign = "middle"
  629. }
  630. if st.HasPad {
  631. props.Pad = []float64{units.Round2(st.PadT), units.Round2(st.PadR), units.Round2(st.PadB), units.Round2(st.PadL)}
  632. }
  633. if r := odfLenToPx(n.attr("corner-radius")) * cv.sx; r > 0 {
  634. props.Radius = units.Round2(r)
  635. }
  636. cv.add(slide, &model.Object{Type: "shape", X: x, Y: y, W: w, H: h, Rot: rot, Props: props})
  637. }
  638. // odpCustomShapeKind maps a draw:custom-shape's enhanced geometry type onto
  639. // the shape the editor draws for it - the inverse of odpShapeTypes
  640. // (odp_writer.go), plus the spellings other producers use.
  641. func odpCustomShapeKind(n *onode) string {
  642. eg := n.first("enhanced-geometry")
  643. if eg == nil {
  644. return "rect"
  645. }
  646. switch eg.attr("type") {
  647. case "ellipse", "circle":
  648. return "ellipse"
  649. case "round-rectangle", "rounded-rectangle":
  650. return "roundRect"
  651. case "triangle":
  652. return "triangle"
  653. case "star":
  654. return "star5"
  655. case "isosceles-triangle":
  656. return "triangle"
  657. case "right-triangle":
  658. return "rtTriangle"
  659. case "diamond":
  660. return "diamond"
  661. case "pentagon":
  662. return "pentagon"
  663. case "hexagon":
  664. return "hexagon"
  665. case "octagon":
  666. return "octagon"
  667. case "parallelogram":
  668. return "parallelogram"
  669. case "trapezoid":
  670. return "trapezoid"
  671. case "cross":
  672. return "plus"
  673. case "pentagon-right":
  674. return "chevron"
  675. case "can":
  676. return "can"
  677. case "cube":
  678. return "cube"
  679. case "ring":
  680. return "donut"
  681. case "forbidden":
  682. return "noSmoking"
  683. case "heart":
  684. return "heart"
  685. case "lightning":
  686. return "lightningBolt"
  687. case "sun":
  688. return "sun"
  689. case "moon":
  690. return "moon"
  691. case "cloud":
  692. return "cloud"
  693. case "smiley":
  694. return "smileyFace"
  695. case "paper":
  696. return "foldedCorner"
  697. case "block-arc":
  698. return "blockArc"
  699. case "arc":
  700. return "arc"
  701. case "pie":
  702. return "pie"
  703. case "frame":
  704. return "frame"
  705. case "right-arrow":
  706. return "rightArrow"
  707. case "left-arrow":
  708. return "leftArrow"
  709. case "up-arrow":
  710. return "upArrow"
  711. case "down-arrow":
  712. return "downArrow"
  713. case "left-right-arrow":
  714. return "leftRightArrow"
  715. case "up-down-arrow":
  716. return "upDownArrow"
  717. case "quad-arrow":
  718. return "quadArrow"
  719. case "notched-right-arrow":
  720. return "notchedRightArrow"
  721. case "striped-right-arrow":
  722. return "stripedRightArrow"
  723. case "circular-arrow":
  724. return "circularArrow"
  725. case "right-arrow-callout":
  726. return "rightArrowCallout"
  727. case "left-arrow-callout":
  728. return "leftArrowCallout"
  729. case "up-arrow-callout":
  730. return "upArrowCallout"
  731. case "down-arrow-callout":
  732. return "downArrowCallout"
  733. case "left-right-arrow-callout":
  734. return "leftRightArrowCallout"
  735. case "star4":
  736. return "star4"
  737. case "star5":
  738. return "star5"
  739. case "star6":
  740. return "star6"
  741. case "star8":
  742. return "star8"
  743. case "star12":
  744. return "star12"
  745. case "star24":
  746. return "star24"
  747. case "bang":
  748. return "irregularSeal1"
  749. case "rectangular-callout":
  750. return "wedgeRectCallout"
  751. case "round-rectangular-callout":
  752. return "wedgeRoundRectCallout"
  753. case "round-callout":
  754. return "wedgeEllipseCallout"
  755. case "cloud-callout":
  756. return "cloudCallout"
  757. case "left-bracket":
  758. return "leftBracket"
  759. case "right-bracket":
  760. return "rightBracket"
  761. case "bracket-pair":
  762. return "bracketPair"
  763. case "left-brace":
  764. return "leftBrace"
  765. case "right-brace":
  766. return "rightBrace"
  767. case "brace-pair":
  768. return "bracePair"
  769. case "mathplus":
  770. return "mathPlus"
  771. case "mathminus":
  772. return "mathMinus"
  773. case "mathmultiply":
  774. return "mathMultiply"
  775. case "mathdivide":
  776. return "mathDivide"
  777. case "mathequal":
  778. return "mathEqual"
  779. case "mathnotequal":
  780. return "mathNotEqual"
  781. case "flowchart-process":
  782. return "flowChartProcess"
  783. case "flowchart-alternate-process":
  784. return "flowChartAlternateProcess"
  785. case "flowchart-decision":
  786. return "flowChartDecision"
  787. case "flowchart-data":
  788. return "flowChartInputOutput"
  789. case "flowchart-predefined-process":
  790. return "flowChartPredefinedProcess"
  791. case "flowchart-internal-storage":
  792. return "flowChartInternalStorage"
  793. case "flowchart-document":
  794. return "flowChartDocument"
  795. case "flowchart-multidocument":
  796. return "flowChartMultidocument"
  797. case "flowchart-terminator":
  798. return "flowChartTerminator"
  799. case "flowchart-preparation":
  800. return "flowChartPreparation"
  801. case "flowchart-manual-input":
  802. return "flowChartManualInput"
  803. case "flowchart-manual-operation":
  804. return "flowChartManualOperation"
  805. case "flowchart-connector":
  806. return "flowChartConnector"
  807. case "flowchart-off-page-connector":
  808. return "flowChartOffpageConnector"
  809. case "flowchart-card":
  810. return "flowChartPunchedCard"
  811. case "flowchart-punched-tape":
  812. return "flowChartPunchedTape"
  813. case "flowchart-summing-junction":
  814. return "flowChartSummingJunction"
  815. case "flowchart-or":
  816. return "flowChartOr"
  817. case "flowchart-collate":
  818. return "flowChartCollate"
  819. case "flowchart-sort":
  820. return "flowChartSort"
  821. case "flowchart-extract":
  822. return "flowChartExtract"
  823. case "flowchart-merge":
  824. return "flowChartMerge"
  825. case "flowchart-delay":
  826. return "flowChartDelay"
  827. case "flowchart-magnetic-disk":
  828. return "flowChartMagneticDisk"
  829. case "flowchart-display":
  830. return "flowChartDisplay"
  831. }
  832. return "rect"
  833. }
  834. func (cv *odpConverter) line(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
  835. x1 := ox + odfLenToPx(n.attr("x1"))*cv.sx*kx
  836. y1 := oy + odfLenToPx(n.attr("y1"))*cv.sy*ky
  837. x2 := ox + odfLenToPx(n.attr("x2"))*cv.sx*kx
  838. y2 := oy + odfLenToPx(n.attr("y2"))*cv.sy*ky
  839. st := cv.styleOf(n)
  840. stroke := st.Stroke
  841. if stroke == "" || stroke == "none" {
  842. stroke = "#333333"
  843. }
  844. sw := st.StrokeW
  845. if sw <= 0 {
  846. sw = 2
  847. }
  848. cv.add(slide, &model.Object{Type: "line", X: x1, Y: y1, W: x2 - x1, H: y2 - y1,
  849. Props: model.Props{Stroke: stroke, StrokeW: units.Round2(sw), Dash: st.Dash,
  850. ArrowEnd: st.ArrowEnd, ArrowStart: st.ArrowHead}})
  851. }
  852. func (cv *odpConverter) table(tbl *onode, slide *model.Slide, x, y, w, h float64, st odpStyle) {
  853. var rows [][]string
  854. var fills [][]string
  855. // relative column widths, so a table keeps its proportions
  856. var colW []float64
  857. var colSum float64
  858. for _, col := range tbl.all("table-column") {
  859. cw := 0.0
  860. if cs, ok := cv.styles[col.attr("style-name")]; ok {
  861. if tp := cs.first("table-column-properties"); tp != nil {
  862. cw = odfLenToPx(tp.attr("column-width"))
  863. }
  864. }
  865. colW = append(colW, cw)
  866. colSum += cw
  867. }
  868. if colSum <= 0 {
  869. colW = nil
  870. } else {
  871. for i := range colW {
  872. colW[i] = units.Round2(colW[i] / colSum * 100)
  873. }
  874. }
  875. for _, tr := range tbl.all("table-row") {
  876. var row, rowFills []string
  877. for _, c := range tr.children {
  878. if c.el == nil || c.el.name != "table-cell" {
  879. continue
  880. }
  881. cellStyle := cv.resolveStyle(c.el.attr("style-name"), st)
  882. fill := ""
  883. if cs, ok := cv.styles[c.el.attr("style-name")]; ok {
  884. if tp := cs.first("table-cell-properties"); tp != nil {
  885. if v := tp.attr("background-color"); strings.HasPrefix(v, "#") {
  886. fill = strings.ToLower(v)
  887. }
  888. }
  889. }
  890. res := cv.buildText(c.el, cellStyle)
  891. row = append(row, res.HTML)
  892. rowFills = append(rowFills, fill)
  893. // a spanned cell occupies the columns it covers
  894. for i := 1; i < int(units.AtofDefault(c.el.attr("number-columns-spanned"), 1)); i++ {
  895. row = append(row, "")
  896. rowFills = append(rowFills, fill)
  897. }
  898. }
  899. if len(row) > 0 {
  900. rows = append(rows, row)
  901. fills = append(fills, rowFills)
  902. }
  903. }
  904. if len(rows) == 0 {
  905. return
  906. }
  907. if !anyFilled(fills) {
  908. fills = nil
  909. }
  910. cv.add(slide, &model.Object{Type: "table", X: x, Y: y, W: w, H: h,
  911. Props: model.Props{Rows: rows, FontSize: units.Round2(units.PtToPx(st.SizePt)),
  912. ColW: colW, CellFill: fills}})
  913. }
  914. /* ---------------- text ---------------- */
  915. // odpText is the rendered HTML of a text body plus its first run's style
  916. type odpText struct {
  917. HTML string
  918. Plain string
  919. First odpStyle
  920. // the whole box is bold / italic / underlined: every run is. The editor
  921. // applies these to the box itself, and a descendant cannot take an
  922. // underline off again, so the first run alone must not decide.
  923. Bold, Italic, Underline bool
  924. }
  925. // buildText renders every paragraph and list under a container into the
  926. // editor's storage HTML, resolving each paragraph's and span's styles
  927. func (cv *odpConverter) buildText(container *onode, base odpStyle) odpText {
  928. out := odpText{First: base}
  929. flags := &odpRunFlags{bold: true, italic: true, under: true}
  930. outer := cv.runs
  931. cv.runs = flags
  932. defer func() { cv.runs = outer }()
  933. var sb strings.Builder
  934. var plain []string
  935. first := true
  936. counters := map[int]int{}
  937. var walk func(n *onode, depth int, listStyle string)
  938. walk = func(n *onode, depth int, listStyle string) {
  939. for _, c := range n.children {
  940. if c.el == nil {
  941. continue
  942. }
  943. switch c.el.name {
  944. case "p", "h":
  945. ps := cv.resolveStyle(c.el.attr("style-name"), base)
  946. bullet := ""
  947. if depth > 0 {
  948. bullet, counters = odpListMarker(cv.lists[listStyle], depth, counters)
  949. }
  950. html, text, firstRun := cv.paragraphHTML(c.el, ps, bullet, depth)
  951. if first && strings.TrimSpace(text) != "" {
  952. out.First = firstRun
  953. first = false
  954. }
  955. sb.WriteString(html)
  956. plain = append(plain, text)
  957. case "list":
  958. ls := c.el.attr("style-name")
  959. if ls == "" {
  960. ls = listStyle
  961. }
  962. if depth == 0 {
  963. counters = map[int]int{}
  964. }
  965. for _, item := range c.el.all("list-item") {
  966. walk(item, depth+1, ls)
  967. }
  968. case "text-box":
  969. walk(c.el, depth, listStyle)
  970. }
  971. }
  972. }
  973. walk(container, 0, "")
  974. out.HTML = sb.String()
  975. out.Plain = strings.Join(plain, "\n")
  976. if strings.TrimSpace(out.Plain) == "" {
  977. out.HTML = ""
  978. }
  979. if flags.any {
  980. out.Bold, out.Italic, out.Underline = flags.bold, flags.italic, flags.under
  981. } else {
  982. out.Bold, out.Italic, out.Underline = out.First.Bold, out.First.Italic, out.First.Underline
  983. }
  984. return out
  985. }
  986. // paragraphHTML renders one text:p / text:h into a storage-HTML block
  987. func (cv *odpConverter) paragraphHTML(p *onode, ps odpStyle, bullet string, depth int) (string, string, odpStyle) {
  988. var runs strings.Builder
  989. var text strings.Builder
  990. firstRun := ps
  991. haveRun := false
  992. minSize := 0.0
  993. var emit func(n *onode, st odpStyle)
  994. emit = func(n *onode, st odpStyle) {
  995. for _, c := range n.children {
  996. if c.el == nil {
  997. if c.text == "" {
  998. continue
  999. }
  1000. if !haveRun {
  1001. firstRun = st
  1002. haveRun = true
  1003. }
  1004. if minSize == 0 || st.SizePt < minSize {
  1005. minSize = st.SizePt
  1006. }
  1007. if f := cv.runs; f != nil && strings.TrimSpace(c.text) != "" {
  1008. f.any = true
  1009. f.bold = f.bold && st.Bold
  1010. f.italic = f.italic && st.Italic
  1011. f.under = f.under && st.Underline
  1012. }
  1013. runs.WriteString(`<span style="` + odpRunCSS(st) + `">` +
  1014. xmltree.Escape(c.text) + `</span>`)
  1015. text.WriteString(c.text)
  1016. continue
  1017. }
  1018. switch c.el.name {
  1019. case "span":
  1020. emit(c.el, cv.resolveStyle(c.el.attr("style-name"), st))
  1021. case "a":
  1022. emit(c.el, st)
  1023. case "line-break":
  1024. runs.WriteString("<br>")
  1025. text.WriteString("\n")
  1026. case "tab":
  1027. runs.WriteString(`<span style="` + odpRunCSS(st) + `">&#9;</span>`)
  1028. text.WriteString("\t")
  1029. case "s":
  1030. n := int(units.AtofDefault(c.el.attr("c"), 1))
  1031. runs.WriteString(strings.Repeat(" ", n))
  1032. text.WriteString(strings.Repeat(" ", n))
  1033. default:
  1034. emit(c.el, st)
  1035. }
  1036. }
  1037. }
  1038. emit(p, ps)
  1039. if minSize <= 0 {
  1040. minSize = ps.SizePt
  1041. }
  1042. var css strings.Builder
  1043. css.WriteString("text-align:" + ps.Align + ";")
  1044. css.WriteString("line-height:" + strconv.FormatFloat(units.Round2(odpLineHeight(ps)), 'f', -1, 64) + ";")
  1045. css.WriteString("font-size:" + units.FmtPx(units.PtToPx(minSize)) + ";")
  1046. if ps.SpaceTop > 0 {
  1047. css.WriteString("margin-top:" + units.FmtPx(ps.SpaceTop) + ";")
  1048. }
  1049. if ps.SpaceBot > 0 {
  1050. css.WriteString("margin-bottom:" + units.FmtPx(ps.SpaceBot) + ";")
  1051. }
  1052. marL := ps.MarginLeft
  1053. if depth > 0 && marL == 0 {
  1054. marL = float64(depth) * 24
  1055. }
  1056. if marL != 0 {
  1057. css.WriteString("padding-left:" + units.FmtPx(marL) + ";")
  1058. }
  1059. body := runs.String()
  1060. marker := ""
  1061. if bullet != "" {
  1062. indent := ps.Indent
  1063. if indent == 0 {
  1064. indent = -18
  1065. }
  1066. css.WriteString("position:relative;")
  1067. bs := firstRun
  1068. bs.Underline, bs.Strike, bs.Highlight = false, false, ""
  1069. marker = `<span style="position:absolute;left:` + units.FmtPx(marL+indent) + `;` +
  1070. odpRunCSS(bs) + `">` + xmltree.Escape(bullet) + `</span>`
  1071. } else if ps.Indent != 0 {
  1072. css.WriteString("text-indent:" + units.FmtPx(ps.Indent) + ";")
  1073. }
  1074. if body == "" {
  1075. body = `<span style="font-size:` + units.FmtPx(units.PtToPx(minSize)) + `">&#8203;</span>`
  1076. }
  1077. return `<div style="` + css.String() + `">` + marker + body + `</div>`,
  1078. text.String(), firstRun
  1079. }
  1080. // odpRunCSS renders the inline style of one text run
  1081. func odpRunCSS(st odpStyle) string {
  1082. var sb strings.Builder
  1083. sb.WriteString("font-size:" + units.FmtPx(units.PtToPx(st.SizePt)) + ";")
  1084. if st.Font != "" {
  1085. sb.WriteString("font-family:" + fonts.StackFor(st.Font, st.Font) + ";")
  1086. }
  1087. // only fo:font-weight sets a weight; a weight word in the family name
  1088. // belongs to that face, not to the CJK text the run falls back for
  1089. // (see runCSS in the pptx reader)
  1090. if st.Bold {
  1091. sb.WriteString("font-weight:700;")
  1092. }
  1093. if st.Italic {
  1094. sb.WriteString("font-style:italic;")
  1095. }
  1096. deco := ""
  1097. if st.Underline {
  1098. deco = "underline"
  1099. }
  1100. if st.Strike {
  1101. if deco != "" {
  1102. deco += " "
  1103. }
  1104. deco += "line-through"
  1105. }
  1106. if deco != "" {
  1107. sb.WriteString("text-decoration:" + deco + ";")
  1108. }
  1109. if st.Color != "" {
  1110. sb.WriteString("color:" + st.Color + ";")
  1111. }
  1112. if st.Highlight != "" {
  1113. sb.WriteString("background-color:" + st.Highlight + ";")
  1114. }
  1115. return sb.String()
  1116. }
  1117. // odpListMarker renders the marker of a list level, advancing the counter
  1118. // of a numbered list
  1119. func odpListMarker(listStyle *onode, depth int, counters map[int]int) (string, map[int]int) {
  1120. bullet := "•"
  1121. if listStyle != nil {
  1122. for _, lvl := range listStyle.children {
  1123. if lvl.el == nil {
  1124. continue
  1125. }
  1126. if int(units.AtofDefault(lvl.el.attr("level"), 0)) != depth {
  1127. continue
  1128. }
  1129. switch lvl.el.name {
  1130. case "list-level-style-bullet":
  1131. if c := lvl.el.attr("bullet-char"); c != "" {
  1132. bullet = c
  1133. }
  1134. case "list-level-style-number":
  1135. counters[depth]++
  1136. return numberedMarker(lvl.el, counters[depth]), counters
  1137. }
  1138. }
  1139. }
  1140. return bullet, counters
  1141. }
  1142. func numberedMarker(lvl *onode, n int) string {
  1143. format := lvl.attr("num-format")
  1144. prefix := lvl.attr("num-prefix")
  1145. suffix := lvl.attr("num-suffix")
  1146. if suffix == "" {
  1147. suffix = "."
  1148. }
  1149. var body string
  1150. switch format {
  1151. case "a":
  1152. body = slidetext.AlphaMarker(n, 'a')
  1153. case "A":
  1154. body = slidetext.AlphaMarker(n, 'A')
  1155. case "i":
  1156. body = strings.ToLower(slidetext.RomanMarker(n))
  1157. case "I":
  1158. body = slidetext.RomanMarker(n)
  1159. default:
  1160. body = strconv.Itoa(n)
  1161. }
  1162. return prefix + body + suffix
  1163. }
  1164. // odfClipFractions turns an fo:clip rect into the editor's crop fractions.
  1165. // ODF states the clip as inset lengths, which is only convertible into
  1166. // fractions when the frame states them as percentages.
  1167. func odfClipFractions(clip string) []float64 {
  1168. clip = strings.TrimSpace(clip)
  1169. if !strings.HasPrefix(clip, "rect(") || !strings.HasSuffix(clip, ")") {
  1170. return nil
  1171. }
  1172. parts := strings.FieldsFunc(clip[5:len(clip)-1], func(r rune) bool {
  1173. return r == ',' || r == ' '
  1174. })
  1175. if len(parts) != 4 {
  1176. return nil
  1177. }
  1178. out := make([]float64, 4)
  1179. // fo:clip lists top, right, bottom, left; the editor wants l,t,r,b
  1180. order := []int{3, 0, 1, 2}
  1181. for i, src := range order {
  1182. v := strings.TrimSpace(parts[src])
  1183. if !strings.HasSuffix(v, "%") {
  1184. return nil
  1185. }
  1186. out[i] = units.Round4(units.ParseNum(v) / 100)
  1187. }
  1188. return out
  1189. }