| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- package office
- /*
- odp_reader.go - Parse an OpenDocument Presentation (.odp) into a
- Presentation, scaled into the 960x540 editor space.
- Frames with text boxes become text objects (paragraph lines joined with
- <br>), draw:image frames become image objects (pictures re-inlined as
- data URLs), draw:rect / draw:ellipse become shapes, draw:line lines and
- framed tables become table objects. Slide backgrounds and speaker
- notes are kept.
- */
- import (
- "encoding/base64"
- "errors"
- "fmt"
- "strings"
- )
- // ParseOdp converts raw .odp bytes into a Presentation
- func ParseOdp(data []byte) (*Presentation, error) {
- files, mime, err := readOdfZip(data)
- if err != nil {
- return nil, err
- }
- if mime != "" && mime != odpMime {
- return nil, errors.New("not an OpenDocument presentation (mimetype " + mime + ")")
- }
- content, ok := files["content.xml"]
- if !ok {
- return nil, errors.New("odp is missing content.xml")
- }
- tree, err := parseOdfXML(content)
- if err != nil {
- return nil, errors.New("cannot parse content.xml: " + err.Error())
- }
- root := tree.first("document-content")
- if root == nil {
- return nil, errors.New("content.xml has no document-content root")
- }
- // source page size -> scale into the 960x540 editor space
- scaleX, scaleY := 1.0, 1.0
- if raw, ok := files["styles.xml"]; ok {
- if st, err := parseOdfXML(raw); err == nil {
- if pp := st.path("document-styles", "automatic-styles", "page-layout", "page-layout-properties"); pp != nil {
- if wPx := odfLenToPx(pp.attr("page-width")); wPx > 0 {
- scaleX = float64(slidePxW) / wPx
- }
- if hPx := odfLenToPx(pp.attr("page-height")); hPx > 0 {
- scaleY = float64(slidePxH) / hPx
- }
- }
- }
- }
- pageBg := map[string]string{}
- shapeStyle := map[string]*onode{}
- if auto := root.first("automatic-styles"); auto != nil {
- for _, st := range auto.all("style") {
- name := st.attr("name")
- if name == "" {
- continue
- }
- switch st.attr("family") {
- case "drawing-page":
- if dp := st.first("drawing-page-properties"); dp != nil {
- if c := dp.attr("fill-color"); strings.HasPrefix(c, "#") {
- pageBg[name] = strings.ToLower(c)
- }
- }
- case "graphic", "presentation":
- shapeStyle[name] = st
- }
- }
- }
- pres := root.path("body", "presentation")
- if pres == nil {
- return nil, errors.New("odp has no presentation body")
- }
- out := &Presentation{Size: []int{slidePxW, slidePxH}, Slides: []*Slide{}}
- cv := &odpConverter{files: files, sx: scaleX, sy: scaleY, shapeStyle: shapeStyle}
- for pi, page := range pres.all("page") {
- slide := &Slide{ID: fmt.Sprintf("s-odp-%d", pi+1), Objects: []*Object{}}
- if bg, ok := pageBg[page.attr("style-name")]; ok {
- slide.Bg = bg
- }
- for _, c := range page.children {
- if c.el == nil {
- continue
- }
- switch c.el.name {
- case "frame":
- cv.frame(c.el, slide)
- case "rect", "ellipse", "custom-shape":
- cv.shape(c.el, slide)
- case "line":
- cv.line(c.el, slide)
- case "notes":
- slide.Notes = strings.TrimSpace(c.el.allText())
- }
- }
- out.Slides = append(out.Slides, slide)
- }
- if len(out.Slides) == 0 {
- return nil, errors.New("no slides found in odp")
- }
- return out, nil
- }
- type odpConverter struct {
- files map[string][]byte
- sx, sy float64
- shapeStyle map[string]*onode
- seq int
- }
- func (cv *odpConverter) geom(n *onode) (x, y, w, h float64) {
- return odfLenToPx(n.attr("x")) * cv.sx, odfLenToPx(n.attr("y")) * cv.sy,
- odfLenToPx(n.attr("width")) * cv.sx, odfLenToPx(n.attr("height")) * cv.sy
- }
- func (cv *odpConverter) nextID() string {
- cv.seq++
- return fmt.Sprintf("o-odp-%d", cv.seq)
- }
- func (cv *odpConverter) frame(n *onode, slide *Slide) {
- x, y, w, h := cv.geom(n)
- if w <= 0 || h <= 0 {
- w, h = 200, 100
- }
- if img := n.first("image"); img != nil {
- raw, ok := cv.files[strings.TrimPrefix(img.attr("href"), "./")]
- if !ok {
- return
- }
- ext := strings.TrimPrefix(strings.ToLower(pathExtOf(img.attr("href"))), ".")
- if ext == "jpg" {
- ext = "jpeg"
- }
- if ext != "png" && ext != "jpeg" && ext != "gif" {
- return
- }
- slide.Objects = append(slide.Objects, &Object{
- ID: cv.nextID(), Type: "image", X: x, Y: y, W: w, H: h,
- Z: len(slide.Objects) + 1,
- Props: Props{Src: "data:image/" + ext + ";base64," +
- base64.StdEncoding.EncodeToString(raw), Fit: "contain"},
- })
- return
- }
- if tbl := n.first("table"); tbl != nil {
- cv.table(tbl, slide, x, y, w, h)
- return
- }
- if tb := n.first("text-box"); tb != nil {
- var lines []string
- for _, p := range tb.all("p") {
- lines = append(lines, p.allText())
- }
- if len(lines) == 0 {
- return
- }
- slide.Objects = append(slide.Objects, &Object{
- ID: cv.nextID(), Type: "text", X: x, Y: y, W: w, H: h,
- Z: len(slide.Objects) + 1,
- Props: Props{HTML: linesToHTML(lines), FontSize: 24, Align: "left"},
- })
- }
- }
- func (cv *odpConverter) styleFill(n *onode) (fill, stroke string, strokeW float64) {
- st, ok := cv.shapeStyle[n.attr("style-name")]
- if !ok {
- return "", "", 0
- }
- gp := st.first("graphic-properties")
- if gp == nil {
- return "", "", 0
- }
- if c := gp.attr("fill-color"); strings.HasPrefix(c, "#") {
- fill = strings.ToLower(c)
- }
- if c := gp.attr("stroke-color"); strings.HasPrefix(c, "#") {
- stroke = strings.ToLower(c)
- }
- strokeW = odfLenToPx(gp.attr("stroke-width"))
- return fill, stroke, strokeW
- }
- func (cv *odpConverter) shape(n *onode, slide *Slide) {
- x, y, w, h := cv.geom(n)
- if w <= 0 || h <= 0 {
- return
- }
- kind := "rect"
- if n.name == "ellipse" {
- kind = "ellipse"
- } else if n.attr("corner-radius") != "" {
- kind = "round"
- }
- fill, stroke, sw := cv.styleFill(n)
- if fill == "" {
- fill = "#e07b1f"
- }
- slide.Objects = append(slide.Objects, &Object{
- ID: cv.nextID(), Type: "shape", X: x, Y: y, W: w, H: h,
- Z: len(slide.Objects) + 1,
- Props: Props{Kind: kind, Fill: fill, Stroke: stroke, StrokeW: sw,
- Text: strings.TrimSpace(n.allText())},
- })
- }
- func (cv *odpConverter) line(n *onode, slide *Slide) {
- x1 := odfLenToPx(n.attr("x1")) * cv.sx
- y1 := odfLenToPx(n.attr("y1")) * cv.sy
- x2 := odfLenToPx(n.attr("x2")) * cv.sx
- y2 := odfLenToPx(n.attr("y2")) * cv.sy
- _, stroke, sw := cv.styleFill(n)
- if stroke == "" {
- stroke = "#333333"
- }
- if sw <= 0 {
- sw = 2
- }
- slide.Objects = append(slide.Objects, &Object{
- ID: cv.nextID(), Type: "line", X: x1, Y: y1, W: x2 - x1, H: y2 - y1,
- Z: len(slide.Objects) + 1,
- Props: Props{Stroke: stroke, StrokeW: sw},
- })
- }
- func (cv *odpConverter) table(tbl *onode, slide *Slide, x, y, w, h float64) {
- var rows [][]string
- for _, tr := range tbl.all("table-row") {
- var row []string
- for _, c := range tr.children {
- if c.el == nil || c.el.name != "table-cell" {
- continue
- }
- row = append(row, xmlEscape(strings.TrimSpace(c.el.allText())))
- }
- if len(row) > 0 {
- rows = append(rows, row)
- }
- }
- if len(rows) == 0 {
- return
- }
- slide.Objects = append(slide.Objects, &Object{
- ID: cv.nextID(), Type: "table", X: x, Y: y, W: w, H: h,
- Z: len(slide.Objects) + 1,
- Props: Props{Rows: rows, FontSize: 16},
- })
- }
|