| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245 |
- package odf
- /*
- odp_reader.go - Parse an OpenDocument Presentation (.odp) into a
- Presentation, scaled into the editor space (960 wide, the page's shape).
- ODF states appearance the same way PresentationML does: almost never on
- the element itself. A paragraph names a style, that style names a parent,
- the parent lives in styles.xml, and a frame with presentation:class="title"
- takes its position and its typography from the matching placeholder on the
- master page. Reading only what a shape spells out - which is what the
- first version of this reader did - gives every text box the same 24px
- default and drops the deck's whole design.
- So the reader resolves, lowest priority first:
- styles.xml office:styles the named style and its parents
- styles.xml automatic-styles the master page's own styles
- content.xml automatic-styles the per-document generated styles
- the paragraph's style, then the span's
- and draws each page on top of its master page's decoration, the way an
- ODF viewer does.
- */
- import (
- "encoding/base64"
- "errors"
- "fmt"
- "math"
- "strconv"
- "strings"
- "imuslab.com/arozos/mod/office/internal/fonts"
- "imuslab.com/arozos/mod/office/internal/slidetext"
- "imuslab.com/arozos/mod/office/internal/units"
- "imuslab.com/arozos/mod/office/internal/xmltree"
- "imuslab.com/arozos/mod/office/metafile"
- "imuslab.com/arozos/mod/office/model"
- )
- // odpStyle is a resolved ODF style: the graphic, paragraph and text
- // properties that matter to the editor, folded down the parent chain
- type odpStyle struct {
- // text
- SizePt float64
- Font string
- Bold bool
- Italic bool
- Underline bool
- Strike bool
- Color string
- Highlight string
- // paragraph
- Align string
- LineHeight float64 // unitless
- SpaceTop float64 // px
- SpaceBot float64 // px
- MarginLeft float64 // px
- Indent float64 // px
- // graphic
- Fill string // "" unset, "none", or "#rrggbb"
- Stroke string
- StrokeW float64
- Dash bool
- VAlign string // top | middle | bottom
- PadT float64
- PadR float64
- PadB float64
- PadL float64
- HasPad bool
- AutoGrow bool
- Rotation float64
- ArrowEnd bool
- ArrowHead bool
- }
- var odpDefaultStyle = odpStyle{SizePt: 18, Align: "left", Color: "#000000"}
- // ParseOdp converts raw .odp bytes into a Presentation
- func ParseOdp(data []byte) (*model.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")
- }
- cv := &odpConverter{
- files: files,
- sx: 1,
- sy: 1,
- styles: map[string]*onode{},
- lists: map[string]*onode{},
- pageBg: map[string]string{},
- }
- pageW, pageH := model.SlidePxW, model.SlidePxH
- var stylesRoot *onode
- if raw, ok := files["styles.xml"]; ok {
- if st, err := parseOdfXML(raw); err == nil {
- stylesRoot = st.first("document-styles")
- }
- }
- if stylesRoot != nil {
- // source page size -> the editor's: 960 wide, the page's shape
- for _, auto := range []*onode{stylesRoot.first("automatic-styles")} {
- if auto == nil {
- continue
- }
- for _, pl := range auto.all("page-layout") {
- pp := pl.first("page-layout-properties")
- if pp == nil {
- continue
- }
- wPx := odfLenToPx(pp.attr("page-width"))
- hPx := odfLenToPx(pp.attr("page-height"))
- if wPx > 0 && hPx > 0 {
- pageW, pageH = model.SlideSizeFor(wPx, hPx)
- cv.sx = float64(pageW) / wPx
- cv.sy = float64(pageH) / hPx
- }
- }
- }
- cv.collectStyles(stylesRoot.first("styles"))
- cv.collectStyles(stylesRoot.first("automatic-styles"))
- if ms := stylesRoot.first("master-styles"); ms != nil {
- cv.masters = map[string]*onode{}
- for _, mp := range ms.all("master-page") {
- cv.masters[mp.attr("name")] = mp
- }
- }
- }
- cv.collectStyles(root.first("automatic-styles"))
- pres := root.path("body", "presentation")
- if pres == nil {
- return nil, errors.New("odp has no presentation body")
- }
- out := &model.Presentation{Size: []int{pageW, pageH}, Slides: []*model.Slide{}}
- for pi, page := range pres.all("page") {
- slide := &model.Slide{ID: fmt.Sprintf("s-odp-%d", pi+1), Objects: []*model.Object{}}
- master := cv.masterOf(page)
- slide.Bg = cv.backgroundOf(page, master)
- cv.master = master
- // the master page's own drawing sits underneath the slide's
- if master != nil {
- cv.walkPage(master, slide, true)
- }
- cv.walkPage(page, slide, false)
- for _, c := range page.children {
- if c.el != nil && c.el.name == "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
- styles map[string]*onode
- lists map[string]*onode
- masters map[string]*onode
- master *onode
- pageBg map[string]string
- seq int
- // runs of the text body being built (see buildText)
- runs *odpRunFlags
- }
- // odpRunFlags gathers whether every run of a text body is bold / italic /
- // underlined - the only case in which the box itself may say so
- type odpRunFlags struct {
- any, bold, italic, under bool
- }
- // collectStyles indexes every style:style and text:list-style in a container
- func (cv *odpConverter) collectStyles(container *onode) {
- if container == nil {
- return
- }
- for _, st := range container.all("style") {
- if n := st.attr("name"); n != "" {
- cv.styles[n] = st
- }
- }
- for _, st := range container.all("list-style") {
- if n := st.attr("name"); n != "" {
- cv.lists[n] = st
- }
- }
- for _, st := range container.all("default-style") {
- if f := st.attr("family"); f != "" {
- cv.styles["#default-"+f] = st
- }
- }
- for _, dp := range container.all("style") {
- if dp.attr("family") != "drawing-page" {
- continue
- }
- if props := dp.first("drawing-page-properties"); props != nil {
- if c := props.attr("fill-color"); strings.HasPrefix(c, "#") {
- cv.pageBg[dp.attr("name")] = strings.ToLower(c)
- }
- }
- }
- }
- func (cv *odpConverter) masterOf(page *onode) *onode {
- if cv.masters == nil {
- return nil
- }
- return cv.masters[page.attr("master-page-name")]
- }
- // backgroundOf resolves a page's fill, falling back to its master's
- func (cv *odpConverter) backgroundOf(page, master *onode) string {
- if bg, ok := cv.pageBg[page.attr("style-name")]; ok {
- return bg
- }
- if master != nil {
- if bg, ok := cv.pageBg[master.attr("style-name")]; ok {
- return bg
- }
- if bg, ok := cv.pageBg[master.attr("page-layout-name")]; ok {
- return bg
- }
- }
- return ""
- }
- // walkPage draws every shape on a page. When masterOnly is set the
- // placeholders are skipped: on a master page they are prototypes that
- // state where a slide's title and body go, not content of their own.
- func (cv *odpConverter) walkPage(page *onode, slide *model.Slide, masterOnly bool) {
- for _, c := range page.children {
- if c.el == nil {
- continue
- }
- if masterOnly && c.el.attr("class") != "" {
- continue
- }
- cv.drawShape(c.el, slide, 0, 0, 1, 1)
- }
- }
- // drawShape converts one drawing element, recursing into groups. ox/oy and
- // kx/ky carry the transform a draw:g imposes on its children.
- func (cv *odpConverter) drawShape(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
- switch n.name {
- case "frame":
- cv.frame(n, slide, ox, oy, kx, ky)
- case "rect", "ellipse", "circle", "custom-shape", "polygon", "regular-polygon":
- cv.shape(n, slide, ox, oy, kx, ky)
- case "line", "connector":
- cv.line(n, slide, ox, oy, kx, ky)
- case "g":
- // a group has no transform of its own in ODF - its children carry
- // absolute coordinates - so it is simply flattened
- for _, c := range n.children {
- if c.el != nil {
- cv.drawShape(c.el, slide, ox, oy, kx, ky)
- }
- }
- }
- }
- /* ---------------- style resolution ---------------- */
- // resolveStyle folds a style and its parent chain into one odpStyle
- func (cv *odpConverter) resolveStyle(name string, base odpStyle) odpStyle {
- if name == "" {
- return base
- }
- // walk to the root of the parent chain first, then apply downwards so
- // the most specific style wins
- var chain []*onode
- seen := map[string]bool{}
- for cur := name; cur != "" && !seen[cur]; {
- seen[cur] = true
- st, ok := cv.styles[cur]
- if !ok {
- break
- }
- chain = append([]*onode{st}, chain...)
- cur = st.attr("parent-style-name")
- }
- out := base
- for _, st := range chain {
- cv.applyStyleNode(&out, st)
- }
- return out
- }
- // applyStyleNode folds one style:style element's properties into dst
- func (cv *odpConverter) applyStyleNode(dst *odpStyle, st *onode) {
- if tp := st.first("text-properties"); tp != nil {
- applyOdfTextProps(dst, tp)
- }
- if pp := st.first("paragraph-properties"); pp != nil {
- cv.applyOdfParaProps(dst, pp)
- }
- if gp := st.first("graphic-properties"); gp != nil {
- cv.applyOdfGraphicProps(dst, gp)
- }
- }
- func applyOdfTextProps(dst *odpStyle, tp *onode) {
- if v := tp.attr("font-size"); v != "" {
- if pt := odfFontSizePt(v, dst.SizePt); pt > 0 {
- dst.SizePt = pt
- }
- }
- if v := tp.attr("font-name"); v != "" {
- dst.Font = v
- }
- if v := tp.attr("font-family"); v != "" {
- dst.Font = strings.Trim(v, "'\"")
- }
- if v := tp.attr("font-weight"); v != "" {
- dst.Bold = v == "bold" || units.ParseNum(v) >= 600
- }
- if v := tp.attr("font-style"); v != "" {
- dst.Italic = v == "italic" || v == "oblique"
- }
- if v := tp.attr("text-underline-style"); v != "" {
- dst.Underline = v != "none"
- }
- if v := tp.attr("text-line-through-style"); v != "" {
- dst.Strike = v != "none"
- }
- if v := tp.attr("color"); strings.HasPrefix(v, "#") {
- dst.Color = strings.ToLower(v)
- }
- if v := tp.attr("background-color"); strings.HasPrefix(v, "#") {
- dst.Highlight = strings.ToLower(v)
- }
- }
- func (cv *odpConverter) applyOdfParaProps(dst *odpStyle, pp *onode) {
- switch pp.attr("text-align") {
- case "center":
- dst.Align = "center"
- case "end", "right":
- dst.Align = "right"
- case "justify":
- dst.Align = "justify"
- case "start", "left":
- dst.Align = "left"
- }
- if v := pp.attr("line-height"); v != "" {
- if strings.HasSuffix(v, "%") {
- dst.LineHeight = units.ParseNum(v) / 100 * model.LineHeightFactor
- } else if px := odfLenToPx(v); px > 0 && dst.SizePt > 0 {
- dst.LineHeight = px / units.PtToPx(dst.SizePt)
- }
- }
- if v := pp.attr("margin-top"); v != "" {
- dst.SpaceTop = odfLenToPx(v) * cv.sy
- }
- if v := pp.attr("margin-bottom"); v != "" {
- dst.SpaceBot = odfLenToPx(v) * cv.sy
- }
- if v := pp.attr("margin-left"); v != "" {
- dst.MarginLeft = odfLenToPx(v) * cv.sx
- }
- if v := pp.attr("text-indent"); v != "" {
- dst.Indent = odfLenToPx(v) * cv.sx
- }
- }
- func (cv *odpConverter) applyOdfGraphicProps(dst *odpStyle, gp *onode) {
- switch gp.attr("fill") {
- case "none":
- dst.Fill = "none"
- case "solid", "bitmap", "gradient", "hatch":
- if c := gp.attr("fill-color"); strings.HasPrefix(c, "#") {
- dst.Fill = strings.ToLower(c)
- }
- default:
- if c := gp.attr("fill-color"); strings.HasPrefix(c, "#") {
- dst.Fill = strings.ToLower(c)
- }
- }
- switch gp.attr("stroke") {
- case "none":
- dst.Stroke = "none"
- dst.StrokeW = 0
- case "dash":
- dst.Dash = true
- fallthrough
- case "solid":
- if c := gp.attr("stroke-color"); strings.HasPrefix(c, "#") {
- dst.Stroke = strings.ToLower(c)
- } else if dst.Stroke == "" || dst.Stroke == "none" {
- dst.Stroke = "#000000"
- }
- if w := odfLenToPx(gp.attr("stroke-width")) * cv.sx; w > 0 {
- dst.StrokeW = w
- } else if dst.StrokeW <= 0 {
- dst.StrokeW = 1
- }
- }
- switch gp.attr("textarea-vertical-align") {
- case "middle":
- dst.VAlign = "middle"
- case "bottom":
- dst.VAlign = "bottom"
- case "top":
- dst.VAlign = "top"
- }
- for attr, field := range map[string]*float64{
- "padding-top": &dst.PadT, "padding-right": &dst.PadR,
- "padding-bottom": &dst.PadB, "padding-left": &dst.PadL,
- } {
- if v := gp.attr(attr); v != "" {
- *field = odfLenToPx(v) * cv.sx
- dst.HasPad = true
- }
- }
- if v := gp.attr("padding"); v != "" {
- p := odfLenToPx(v) * cv.sx
- dst.PadT, dst.PadR, dst.PadB, dst.PadL = p, p, p, p
- dst.HasPad = true
- }
- if v := gp.attr("marker-end"); v != "" && v != "none" {
- dst.ArrowEnd = true
- }
- if v := gp.attr("marker-start"); v != "" && v != "none" {
- dst.ArrowHead = true
- }
- }
- // odfFontSizePt reads a font size, which may be relative ("120%")
- func odfFontSizePt(v string, current float64) float64 {
- if strings.HasSuffix(v, "%") {
- return current * units.ParseNum(v) / 100
- }
- if strings.HasSuffix(v, "pt") {
- return units.ParseNum(v)
- }
- if px := odfLenToPx(v); px > 0 {
- return px * 0.75
- }
- return 0
- }
- /* ---------------- geometry ---------------- */
- func (cv *odpConverter) geom(n *onode, ox, oy, kx, ky float64) (x, y, w, h float64) {
- x = ox + odfLenToPx(n.attr("x"))*cv.sx*kx
- y = oy + odfLenToPx(n.attr("y"))*cv.sy*ky
- w = odfLenToPx(n.attr("width")) * cv.sx * kx
- h = odfLenToPx(n.attr("height")) * cv.sy * ky
- return x, y, w, h
- }
- // odfRotation reads the rotation out of a draw:transform attribute, in
- // degrees clockwise (ODF states radians counter-clockwise)
- func odfRotation(n *onode) (deg float64, tx, ty float64) {
- t := n.attr("transform")
- if t == "" {
- return 0, 0, 0
- }
- if i := strings.Index(t, "rotate"); i >= 0 {
- if open := strings.Index(t[i:], "("); open >= 0 {
- rest := t[i+open+1:]
- if close := strings.Index(rest, ")"); close >= 0 {
- deg = -units.ParseNum(strings.TrimSpace(rest[:close])) * 180 / math.Pi
- }
- }
- }
- if i := strings.Index(t, "translate"); i >= 0 {
- if open := strings.Index(t[i:], "("); open >= 0 {
- rest := t[i+open+1:]
- if close := strings.Index(rest, ")"); close >= 0 {
- parts := strings.Fields(strings.TrimSpace(rest[:close]))
- if len(parts) == 2 {
- tx, ty = odfLenToPx(parts[0]), odfLenToPx(parts[1])
- }
- }
- }
- }
- return deg, tx, ty
- }
- func (cv *odpConverter) nextID() string {
- cv.seq++
- return fmt.Sprintf("o-odp-%d", cv.seq)
- }
- func (cv *odpConverter) add(slide *model.Slide, o *model.Object) {
- o.ID = cv.nextID()
- o.Z = len(slide.Objects) + 1
- slide.Objects = append(slide.Objects, o)
- }
- // styleOf resolves the drawing style of a shape, including the
- // presentation style a placeholder inherits
- func (cv *odpConverter) styleOf(n *onode) odpStyle {
- st := odpDefaultStyle
- st = cv.resolveStyle("#default-graphic", st)
- if ps := n.attr("class"); ps != "" && cv.master != nil {
- // a placeholder takes the master's matching frame's style
- if mf := cv.masterFrame(ps); mf != nil {
- st = cv.resolveStyle(mf.attr("style-name"), st)
- st = cv.resolveStyle(mf.attr("text-style-name"), st)
- }
- }
- st = cv.resolveStyle(n.attr("style-name"), st)
- st = cv.resolveStyle(n.attr("text-style-name"), st)
- return st
- }
- // masterFrame finds the placeholder frame of a given class on the master
- func (cv *odpConverter) masterFrame(class string) *onode {
- if cv.master == nil {
- return nil
- }
- for _, c := range cv.master.children {
- if c.el != nil && c.el.name == "frame" && c.el.attr("class") == class {
- return c.el
- }
- }
- return nil
- }
- /* ---------------- shapes ---------------- */
- func (cv *odpConverter) frame(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
- x, y, w, h := cv.geom(n, ox, oy, kx, ky)
- if (w <= 0 || h <= 0) && n.attr("class") != "" {
- // a placeholder with no geometry of its own takes the master's
- if mf := cv.masterFrame(n.attr("class")); mf != nil {
- x, y, w, h = cv.geom(mf, ox, oy, kx, ky)
- }
- }
- if w <= 0 || h <= 0 {
- return
- }
- rot, _, _ := odfRotation(n)
- st := cv.styleOf(n)
- if img := n.first("image"); img != nil {
- href := strings.TrimPrefix(img.attr("href"), "./")
- raw, ok := cv.files[href]
- if !ok {
- return
- }
- ext := strings.TrimPrefix(strings.ToLower(pathExtOf(href)), ".")
- if ext == "jpg" {
- ext = "jpeg"
- }
- if ext == "wmf" || ext == "emf" {
- if raw, ext, ok = metafile.BrowserPicture(raw, ext); !ok {
- return
- }
- }
- if ext != "png" && ext != "jpeg" && ext != "gif" {
- return
- }
- props := model.Props{Src: "data:image/" + ext + ";base64," +
- base64.StdEncoding.EncodeToString(raw), Fit: "fill"}
- if c := odfClipFractions(n.attr("clip")); c != nil {
- props.Crop = c
- }
- cv.add(slide, &model.Object{Type: "image", X: x, Y: y, W: w, H: h, Rot: rot, Props: props})
- return
- }
- if tbl := n.first("table"); tbl != nil {
- cv.table(tbl, slide, x, y, w, h, st)
- return
- }
- if tb := n.first("text-box"); tb != nil {
- res := cv.buildText(tb, st)
- if res.HTML == "" {
- return
- }
- cv.add(slide, &model.Object{Type: "text", X: x, Y: y, W: w, H: h, Rot: rot,
- Props: cv.textProps(res, st)})
- }
- }
- // textProps assembles the editor properties of a text body
- func (cv *odpConverter) textProps(res odpText, st odpStyle) model.Props {
- p := model.Props{
- HTML: res.HTML, FontSize: units.Round2(units.PtToPx(res.First.SizePt)),
- Color: res.First.Color, Align: res.First.Align,
- Bold: res.Bold, Italic: res.Italic,
- Underline: res.Underline,
- FontFamily: fonts.StackFor(res.First.Font, res.First.Font),
- VAlign: st.VAlign,
- LineHeight: units.Round2(odpLineHeight(res.First)),
- }
- if p.VAlign == "" {
- p.VAlign = "top"
- }
- if st.HasPad {
- p.Pad = []float64{units.Round2(st.PadT), units.Round2(st.PadR), units.Round2(st.PadB), units.Round2(st.PadL)}
- }
- return p
- }
- func odpLineHeight(st odpStyle) float64 {
- if st.LineHeight > 0 {
- return st.LineHeight
- }
- return model.LineHeightFactor
- }
- func (cv *odpConverter) shape(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
- x, y, w, h := cv.geom(n, ox, oy, kx, ky)
- if w <= 0 || h <= 0 {
- return
- }
- rot, _, _ := odfRotation(n)
- st := cv.styleOf(n)
- kind := "rect"
- switch n.name {
- case "ellipse", "circle":
- kind = "ellipse"
- case "polygon", "regular-polygon":
- kind = "diamond"
- case "custom-shape":
- kind = odpCustomShapeKind(n)
- default:
- if n.attr("corner-radius") != "" {
- kind = "roundRect"
- }
- }
- fill := st.Fill
- if fill == "" {
- fill = "#e07b1f"
- }
- stroke := st.Stroke
- if stroke == "none" {
- stroke = ""
- }
- res := cv.buildText(n, st)
- props := model.Props{Kind: kind, Fill: fill, Stroke: stroke, StrokeW: units.Round2(st.StrokeW),
- Dash: st.Dash, HTML: res.HTML, Text: res.Plain,
- TextColor: res.First.Color, FontSize: units.Round2(units.PtToPx(res.First.SizePt)),
- FontFamily: fonts.StackFor(res.First.Font, res.First.Font),
- Bold: res.Bold, Italic: res.Italic,
- Align: res.First.Align, VAlign: st.VAlign,
- LineHeight: units.Round2(odpLineHeight(res.First)),
- }
- if props.VAlign == "" {
- props.VAlign = "middle"
- }
- if st.HasPad {
- props.Pad = []float64{units.Round2(st.PadT), units.Round2(st.PadR), units.Round2(st.PadB), units.Round2(st.PadL)}
- }
- if r := odfLenToPx(n.attr("corner-radius")) * cv.sx; r > 0 {
- props.Radius = units.Round2(r)
- }
- cv.add(slide, &model.Object{Type: "shape", X: x, Y: y, W: w, H: h, Rot: rot, Props: props})
- }
- // odpCustomShapeKind maps a draw:custom-shape's enhanced geometry type onto
- // the shape the editor draws for it - the inverse of odpShapeTypes
- // (odp_writer.go), plus the spellings other producers use.
- func odpCustomShapeKind(n *onode) string {
- eg := n.first("enhanced-geometry")
- if eg == nil {
- return "rect"
- }
- switch eg.attr("type") {
- case "ellipse", "circle":
- return "ellipse"
- case "round-rectangle", "rounded-rectangle":
- return "roundRect"
- case "triangle":
- return "triangle"
- case "star":
- return "star5"
- case "isosceles-triangle":
- return "triangle"
- case "right-triangle":
- return "rtTriangle"
- case "diamond":
- return "diamond"
- case "pentagon":
- return "pentagon"
- case "hexagon":
- return "hexagon"
- case "octagon":
- return "octagon"
- case "parallelogram":
- return "parallelogram"
- case "trapezoid":
- return "trapezoid"
- case "cross":
- return "plus"
- case "pentagon-right":
- return "chevron"
- case "can":
- return "can"
- case "cube":
- return "cube"
- case "ring":
- return "donut"
- case "forbidden":
- return "noSmoking"
- case "heart":
- return "heart"
- case "lightning":
- return "lightningBolt"
- case "sun":
- return "sun"
- case "moon":
- return "moon"
- case "cloud":
- return "cloud"
- case "smiley":
- return "smileyFace"
- case "paper":
- return "foldedCorner"
- case "block-arc":
- return "blockArc"
- case "arc":
- return "arc"
- case "pie":
- return "pie"
- case "frame":
- return "frame"
- case "right-arrow":
- return "rightArrow"
- case "left-arrow":
- return "leftArrow"
- case "up-arrow":
- return "upArrow"
- case "down-arrow":
- return "downArrow"
- case "left-right-arrow":
- return "leftRightArrow"
- case "up-down-arrow":
- return "upDownArrow"
- case "quad-arrow":
- return "quadArrow"
- case "notched-right-arrow":
- return "notchedRightArrow"
- case "striped-right-arrow":
- return "stripedRightArrow"
- case "circular-arrow":
- return "circularArrow"
- case "right-arrow-callout":
- return "rightArrowCallout"
- case "left-arrow-callout":
- return "leftArrowCallout"
- case "up-arrow-callout":
- return "upArrowCallout"
- case "down-arrow-callout":
- return "downArrowCallout"
- case "left-right-arrow-callout":
- return "leftRightArrowCallout"
- case "star4":
- return "star4"
- case "star5":
- return "star5"
- case "star6":
- return "star6"
- case "star8":
- return "star8"
- case "star12":
- return "star12"
- case "star24":
- return "star24"
- case "bang":
- return "irregularSeal1"
- case "rectangular-callout":
- return "wedgeRectCallout"
- case "round-rectangular-callout":
- return "wedgeRoundRectCallout"
- case "round-callout":
- return "wedgeEllipseCallout"
- case "cloud-callout":
- return "cloudCallout"
- case "left-bracket":
- return "leftBracket"
- case "right-bracket":
- return "rightBracket"
- case "bracket-pair":
- return "bracketPair"
- case "left-brace":
- return "leftBrace"
- case "right-brace":
- return "rightBrace"
- case "brace-pair":
- return "bracePair"
- case "mathplus":
- return "mathPlus"
- case "mathminus":
- return "mathMinus"
- case "mathmultiply":
- return "mathMultiply"
- case "mathdivide":
- return "mathDivide"
- case "mathequal":
- return "mathEqual"
- case "mathnotequal":
- return "mathNotEqual"
- case "flowchart-process":
- return "flowChartProcess"
- case "flowchart-alternate-process":
- return "flowChartAlternateProcess"
- case "flowchart-decision":
- return "flowChartDecision"
- case "flowchart-data":
- return "flowChartInputOutput"
- case "flowchart-predefined-process":
- return "flowChartPredefinedProcess"
- case "flowchart-internal-storage":
- return "flowChartInternalStorage"
- case "flowchart-document":
- return "flowChartDocument"
- case "flowchart-multidocument":
- return "flowChartMultidocument"
- case "flowchart-terminator":
- return "flowChartTerminator"
- case "flowchart-preparation":
- return "flowChartPreparation"
- case "flowchart-manual-input":
- return "flowChartManualInput"
- case "flowchart-manual-operation":
- return "flowChartManualOperation"
- case "flowchart-connector":
- return "flowChartConnector"
- case "flowchart-off-page-connector":
- return "flowChartOffpageConnector"
- case "flowchart-card":
- return "flowChartPunchedCard"
- case "flowchart-punched-tape":
- return "flowChartPunchedTape"
- case "flowchart-summing-junction":
- return "flowChartSummingJunction"
- case "flowchart-or":
- return "flowChartOr"
- case "flowchart-collate":
- return "flowChartCollate"
- case "flowchart-sort":
- return "flowChartSort"
- case "flowchart-extract":
- return "flowChartExtract"
- case "flowchart-merge":
- return "flowChartMerge"
- case "flowchart-delay":
- return "flowChartDelay"
- case "flowchart-magnetic-disk":
- return "flowChartMagneticDisk"
- case "flowchart-display":
- return "flowChartDisplay"
- }
- return "rect"
- }
- func (cv *odpConverter) line(n *onode, slide *model.Slide, ox, oy, kx, ky float64) {
- x1 := ox + odfLenToPx(n.attr("x1"))*cv.sx*kx
- y1 := oy + odfLenToPx(n.attr("y1"))*cv.sy*ky
- x2 := ox + odfLenToPx(n.attr("x2"))*cv.sx*kx
- y2 := oy + odfLenToPx(n.attr("y2"))*cv.sy*ky
- st := cv.styleOf(n)
- stroke := st.Stroke
- if stroke == "" || stroke == "none" {
- stroke = "#333333"
- }
- sw := st.StrokeW
- if sw <= 0 {
- sw = 2
- }
- cv.add(slide, &model.Object{Type: "line", X: x1, Y: y1, W: x2 - x1, H: y2 - y1,
- Props: model.Props{Stroke: stroke, StrokeW: units.Round2(sw), Dash: st.Dash,
- ArrowEnd: st.ArrowEnd, ArrowStart: st.ArrowHead}})
- }
- func (cv *odpConverter) table(tbl *onode, slide *model.Slide, x, y, w, h float64, st odpStyle) {
- var rows [][]string
- var fills [][]string
- // relative column widths, so a table keeps its proportions
- var colW []float64
- var colSum float64
- for _, col := range tbl.all("table-column") {
- cw := 0.0
- if cs, ok := cv.styles[col.attr("style-name")]; ok {
- if tp := cs.first("table-column-properties"); tp != nil {
- cw = odfLenToPx(tp.attr("column-width"))
- }
- }
- colW = append(colW, cw)
- colSum += cw
- }
- if colSum <= 0 {
- colW = nil
- } else {
- for i := range colW {
- colW[i] = units.Round2(colW[i] / colSum * 100)
- }
- }
- for _, tr := range tbl.all("table-row") {
- var row, rowFills []string
- for _, c := range tr.children {
- if c.el == nil || c.el.name != "table-cell" {
- continue
- }
- cellStyle := cv.resolveStyle(c.el.attr("style-name"), st)
- fill := ""
- if cs, ok := cv.styles[c.el.attr("style-name")]; ok {
- if tp := cs.first("table-cell-properties"); tp != nil {
- if v := tp.attr("background-color"); strings.HasPrefix(v, "#") {
- fill = strings.ToLower(v)
- }
- }
- }
- res := cv.buildText(c.el, cellStyle)
- row = append(row, res.HTML)
- rowFills = append(rowFills, fill)
- // a spanned cell occupies the columns it covers
- for i := 1; i < int(units.AtofDefault(c.el.attr("number-columns-spanned"), 1)); i++ {
- row = append(row, "")
- rowFills = append(rowFills, fill)
- }
- }
- if len(row) > 0 {
- rows = append(rows, row)
- fills = append(fills, rowFills)
- }
- }
- if len(rows) == 0 {
- return
- }
- if !anyFilled(fills) {
- fills = nil
- }
- cv.add(slide, &model.Object{Type: "table", X: x, Y: y, W: w, H: h,
- Props: model.Props{Rows: rows, FontSize: units.Round2(units.PtToPx(st.SizePt)),
- ColW: colW, CellFill: fills}})
- }
- /* ---------------- text ---------------- */
- // odpText is the rendered HTML of a text body plus its first run's style
- type odpText struct {
- HTML string
- Plain string
- First odpStyle
- // the whole box is bold / italic / underlined: every run is. The editor
- // applies these to the box itself, and a descendant cannot take an
- // underline off again, so the first run alone must not decide.
- Bold, Italic, Underline bool
- }
- // buildText renders every paragraph and list under a container into the
- // editor's storage HTML, resolving each paragraph's and span's styles
- func (cv *odpConverter) buildText(container *onode, base odpStyle) odpText {
- out := odpText{First: base}
- flags := &odpRunFlags{bold: true, italic: true, under: true}
- outer := cv.runs
- cv.runs = flags
- defer func() { cv.runs = outer }()
- var sb strings.Builder
- var plain []string
- first := true
- counters := map[int]int{}
- var walk func(n *onode, depth int, listStyle string)
- walk = func(n *onode, depth int, listStyle string) {
- for _, c := range n.children {
- if c.el == nil {
- continue
- }
- switch c.el.name {
- case "p", "h":
- ps := cv.resolveStyle(c.el.attr("style-name"), base)
- bullet := ""
- if depth > 0 {
- bullet, counters = odpListMarker(cv.lists[listStyle], depth, counters)
- }
- html, text, firstRun := cv.paragraphHTML(c.el, ps, bullet, depth)
- if first && strings.TrimSpace(text) != "" {
- out.First = firstRun
- first = false
- }
- sb.WriteString(html)
- plain = append(plain, text)
- case "list":
- ls := c.el.attr("style-name")
- if ls == "" {
- ls = listStyle
- }
- if depth == 0 {
- counters = map[int]int{}
- }
- for _, item := range c.el.all("list-item") {
- walk(item, depth+1, ls)
- }
- case "text-box":
- walk(c.el, depth, listStyle)
- }
- }
- }
- walk(container, 0, "")
- out.HTML = sb.String()
- out.Plain = strings.Join(plain, "\n")
- if strings.TrimSpace(out.Plain) == "" {
- out.HTML = ""
- }
- if flags.any {
- out.Bold, out.Italic, out.Underline = flags.bold, flags.italic, flags.under
- } else {
- out.Bold, out.Italic, out.Underline = out.First.Bold, out.First.Italic, out.First.Underline
- }
- return out
- }
- // paragraphHTML renders one text:p / text:h into a storage-HTML block
- func (cv *odpConverter) paragraphHTML(p *onode, ps odpStyle, bullet string, depth int) (string, string, odpStyle) {
- var runs strings.Builder
- var text strings.Builder
- firstRun := ps
- haveRun := false
- minSize := 0.0
- var emit func(n *onode, st odpStyle)
- emit = func(n *onode, st odpStyle) {
- for _, c := range n.children {
- if c.el == nil {
- if c.text == "" {
- continue
- }
- if !haveRun {
- firstRun = st
- haveRun = true
- }
- if minSize == 0 || st.SizePt < minSize {
- minSize = st.SizePt
- }
- if f := cv.runs; f != nil && strings.TrimSpace(c.text) != "" {
- f.any = true
- f.bold = f.bold && st.Bold
- f.italic = f.italic && st.Italic
- f.under = f.under && st.Underline
- }
- runs.WriteString(`<span style="` + odpRunCSS(st) + `">` +
- xmltree.Escape(c.text) + `</span>`)
- text.WriteString(c.text)
- continue
- }
- switch c.el.name {
- case "span":
- emit(c.el, cv.resolveStyle(c.el.attr("style-name"), st))
- case "a":
- emit(c.el, st)
- case "line-break":
- runs.WriteString("<br>")
- text.WriteString("\n")
- case "tab":
- runs.WriteString(`<span style="` + odpRunCSS(st) + `">	</span>`)
- text.WriteString("\t")
- case "s":
- n := int(units.AtofDefault(c.el.attr("c"), 1))
- runs.WriteString(strings.Repeat(" ", n))
- text.WriteString(strings.Repeat(" ", n))
- default:
- emit(c.el, st)
- }
- }
- }
- emit(p, ps)
- if minSize <= 0 {
- minSize = ps.SizePt
- }
- var css strings.Builder
- css.WriteString("text-align:" + ps.Align + ";")
- css.WriteString("line-height:" + strconv.FormatFloat(units.Round2(odpLineHeight(ps)), 'f', -1, 64) + ";")
- css.WriteString("font-size:" + units.FmtPx(units.PtToPx(minSize)) + ";")
- if ps.SpaceTop > 0 {
- css.WriteString("margin-top:" + units.FmtPx(ps.SpaceTop) + ";")
- }
- if ps.SpaceBot > 0 {
- css.WriteString("margin-bottom:" + units.FmtPx(ps.SpaceBot) + ";")
- }
- marL := ps.MarginLeft
- if depth > 0 && marL == 0 {
- marL = float64(depth) * 24
- }
- if marL != 0 {
- css.WriteString("padding-left:" + units.FmtPx(marL) + ";")
- }
- body := runs.String()
- marker := ""
- if bullet != "" {
- indent := ps.Indent
- if indent == 0 {
- indent = -18
- }
- css.WriteString("position:relative;")
- bs := firstRun
- bs.Underline, bs.Strike, bs.Highlight = false, false, ""
- marker = `<span style="position:absolute;left:` + units.FmtPx(marL+indent) + `;` +
- odpRunCSS(bs) + `">` + xmltree.Escape(bullet) + `</span>`
- } else if ps.Indent != 0 {
- css.WriteString("text-indent:" + units.FmtPx(ps.Indent) + ";")
- }
- if body == "" {
- body = `<span style="font-size:` + units.FmtPx(units.PtToPx(minSize)) + `">​</span>`
- }
- return `<div style="` + css.String() + `">` + marker + body + `</div>`,
- text.String(), firstRun
- }
- // odpRunCSS renders the inline style of one text run
- func odpRunCSS(st odpStyle) string {
- var sb strings.Builder
- sb.WriteString("font-size:" + units.FmtPx(units.PtToPx(st.SizePt)) + ";")
- if st.Font != "" {
- sb.WriteString("font-family:" + fonts.StackFor(st.Font, st.Font) + ";")
- }
- // only fo:font-weight sets a weight; a weight word in the family name
- // belongs to that face, not to the CJK text the run falls back for
- // (see runCSS in the pptx reader)
- if st.Bold {
- sb.WriteString("font-weight:700;")
- }
- if st.Italic {
- sb.WriteString("font-style:italic;")
- }
- deco := ""
- if st.Underline {
- deco = "underline"
- }
- if st.Strike {
- if deco != "" {
- deco += " "
- }
- deco += "line-through"
- }
- if deco != "" {
- sb.WriteString("text-decoration:" + deco + ";")
- }
- if st.Color != "" {
- sb.WriteString("color:" + st.Color + ";")
- }
- if st.Highlight != "" {
- sb.WriteString("background-color:" + st.Highlight + ";")
- }
- return sb.String()
- }
- // odpListMarker renders the marker of a list level, advancing the counter
- // of a numbered list
- func odpListMarker(listStyle *onode, depth int, counters map[int]int) (string, map[int]int) {
- bullet := "•"
- if listStyle != nil {
- for _, lvl := range listStyle.children {
- if lvl.el == nil {
- continue
- }
- if int(units.AtofDefault(lvl.el.attr("level"), 0)) != depth {
- continue
- }
- switch lvl.el.name {
- case "list-level-style-bullet":
- if c := lvl.el.attr("bullet-char"); c != "" {
- bullet = c
- }
- case "list-level-style-number":
- counters[depth]++
- return numberedMarker(lvl.el, counters[depth]), counters
- }
- }
- }
- return bullet, counters
- }
- func numberedMarker(lvl *onode, n int) string {
- format := lvl.attr("num-format")
- prefix := lvl.attr("num-prefix")
- suffix := lvl.attr("num-suffix")
- if suffix == "" {
- suffix = "."
- }
- var body string
- switch format {
- case "a":
- body = slidetext.AlphaMarker(n, 'a')
- case "A":
- body = slidetext.AlphaMarker(n, 'A')
- case "i":
- body = strings.ToLower(slidetext.RomanMarker(n))
- case "I":
- body = slidetext.RomanMarker(n)
- default:
- body = strconv.Itoa(n)
- }
- return prefix + body + suffix
- }
- // odfClipFractions turns an fo:clip rect into the editor's crop fractions.
- // ODF states the clip as inset lengths, which is only convertible into
- // fractions when the frame states them as percentages.
- func odfClipFractions(clip string) []float64 {
- clip = strings.TrimSpace(clip)
- if !strings.HasPrefix(clip, "rect(") || !strings.HasSuffix(clip, ")") {
- return nil
- }
- parts := strings.FieldsFunc(clip[5:len(clip)-1], func(r rune) bool {
- return r == ',' || r == ' '
- })
- if len(parts) != 4 {
- return nil
- }
- out := make([]float64, 4)
- // fo:clip lists top, right, bottom, left; the editor wants l,t,r,b
- order := []int{3, 0, 1, 2}
- for i, src := range order {
- v := strings.TrimSpace(parts[src])
- if !strings.HasSuffix(v, "%") {
- return nil
- }
- out[i] = units.Round4(units.ParseNum(v) / 100)
- }
- return out
- }
|