/* Package office implements converters between the ArozOS Office suite document models and common office file formats. Currently implemented: PowerPoint (.pptx) import/export for the Slides webapp (src/web/Office/slides). The JSON model mirrors the "body" schema documented in src/web/Office/common/CONTRACT.md and slides.js: { "size": [960, 540], "theme": "clean", "slides": [ { "id", "bg", "notes", "objects": [ { "id", "type": "text|image|shape|line|table|chart", "x","y","w","h","rot","z", "props": {...} } ] } ] } This package is dependency-free (Go standard library only) and shared by the "office" AGI library (src/mod/agi/agi.office.go). Word (.docx) and Excel (.xlsx) converters for the Docs and Sheets webapps will live here too, reusing the same OOXML plumbing. Author: tobychui */ package office import ( "encoding/base64" "encoding/json" "errors" "regexp" "strconv" "strings" ) // EMU (English Metric Units) per pixel at 96 DPI const emuPerPx = 9525 // Slide coordinate space used by the Slides webapp const ( slidePxW = 960 slidePxH = 540 ) // Presentation is the Slides document body type Presentation struct { Size []int `json:"size,omitempty"` Theme string `json:"theme,omitempty"` Slides []*Slide `json:"slides"` } // Slide is a single slide type Slide struct { ID string `json:"id,omitempty"` Bg string `json:"bg,omitempty"` // "" = theme default background Notes string `json:"notes,omitempty"` Objects []*Object `json:"objects"` } // Object is one visual element on a slide type Object struct { ID string `json:"id,omitempty"` Type string `json:"type"` X float64 `json:"x"` Y float64 `json:"y"` W float64 `json:"w"` H float64 `json:"h"` Rot float64 `json:"rot,omitempty"` Z int `json:"z,omitempty"` Props Props `json:"props"` } // Props carries the per-type properties. Fields for every object type are // merged into one struct so the JSON stays schema-compatible with slides.js. type Props struct { // text HTML string `json:"html,omitempty"` FontSize float64 `json:"fontSize,omitempty"` Color string `json:"color,omitempty"` Align string `json:"align,omitempty"` Bold bool `json:"bold,omitempty"` Italic bool `json:"italic,omitempty"` Underline bool `json:"underline,omitempty"` // image Src string `json:"src,omitempty"` Fit string `json:"fit,omitempty"` // shape Kind string `json:"kind,omitempty"` Fill string `json:"fill,omitempty"` Stroke string `json:"stroke,omitempty"` StrokeW float64 `json:"strokeW,omitempty"` Text string `json:"text,omitempty"` TextColor string `json:"textColor,omitempty"` // line Dash bool `json:"dash,omitempty"` ArrowEnd bool `json:"arrowEnd,omitempty"` // table Rows [][]string `json:"rows,omitempty"` HeaderRow bool `json:"headerRow,omitempty"` ColW []float64 `json:"colW,omitempty"` // column widths, percent RowH []float64 `json:"rowH,omitempty"` // row heights, percent // chart (spec kept opaque; Png is a client-side raster for export) Spec json.RawMessage `json:"spec,omitempty"` Png string `json:"png,omitempty"` } // theme background approximations, mirroring THEMES in slides.js // (gradients are approximated by their first color stop for pptx export) var themeBg = map[string]string{ "clean": "FFFFFF", "midnight": "232A36", "ocean": "0F4C75", "sunset": "C0392B", "forest": "0F3D33", "paper": "F6F1E5", } var themeText = map[string]string{ "clean": "202124", "midnight": "E8EAED", "ocean": "F4FAFF", "sunset": "FDF2EC", "forest": "EAFAF1", "paper": "3D3A33", } // PresentationToJSON serializes a Presentation back to the Slides body JSON func PresentationToJSON(p *Presentation) (string, error) { if p == nil { return "", errors.New("nil presentation") } out, err := json.Marshal(p) if err != nil { return "", err } return string(out), nil } // ParsePresentationJSON decodes the Slides body JSON into a Presentation func ParsePresentationJSON(jsonStr string) (*Presentation, error) { p := Presentation{} if err := json.Unmarshal([]byte(jsonStr), &p); err != nil { return nil, errors.New("invalid presentation JSON: " + err.Error()) } if len(p.Slides) == 0 { return nil, errors.New("presentation contains no slides") } return &p, nil } /* ---------- shared small helpers ---------- */ func pxToEmu(px float64) int64 { return int64(px * emuPerPx) } func emuToPx(emu int64, scale float64) float64 { return float64(emu) / emuPerPx * scale } // hexColor normalizes "#rrggbb" to uppercase "RRGGBB"; returns fallback when invalid func hexColor(c string, fallback string) string { c = strings.TrimPrefix(strings.TrimSpace(c), "#") if len(c) == 3 { c = string([]byte{c[0], c[0], c[1], c[1], c[2], c[2]}) } ok, _ := regexp.MatchString("^[0-9a-fA-F]{6}$", c) if !ok { return fallback } return strings.ToUpper(c) } // xmlEscape escapes a string for use inside XML text nodes and attributes func xmlEscape(s string) string { r := strings.NewReplacer( "&", "&", "<", "<", ">", ">", "\"", """, "'", "'", ) return r.Replace(s) } var brRe = regexp.MustCompile(`(?i)`) var blockCloseRe = regexp.MustCompile(`(?i)`) var blockOpenRe = regexp.MustCompile(`(?i)<(div|p|li|h[1-6])(\s[^>]*)?>`) var tagRe = regexp.MustCompile(`<[^>]*>`) var listTagRe = regexp.MustCompile(`(?i)<(/?)(ul|ol|li)(\s[^>]*)?>`) // flattenListMarkers rewrites