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(`` +
xmltree.Escape(c.text) + ``)
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("
")
text.WriteString("\n")
case "tab":
runs.WriteString(` `)
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 = `` + xmltree.Escape(bullet) + ``
} else if ps.Indent != 0 {
css.WriteString("text-indent:" + units.FmtPx(ps.Indent) + ";")
}
if body == "" {
body = ``
}
return `