mirror of
https://github.com/hanzoai/semantic.git
synced 2026-10-30 15:04:19 +00:00
The 256 MiB cap was per part, so a file of a few KB could name one sheet, slide or shared string a hundred times, or decode a part into a tree 33 times its size, and reach gigabytes. A read now spends from one budget: each part's stated size each time it is read, a node per element decoded, and the text each table writes. Every charge checks the caller's context. A part claiming a size past int64 was let through as negative; it is refused. Style levels resolve in one walk, not one per style. Co-authored-by: Hanzo Dev <[email protected]>
246 lines
5.4 KiB
Go
246 lines
5.4 KiB
Go
package parse
|
|
|
|
import (
|
|
"context"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
|
|
"github.com/hanzoai/semantic"
|
|
)
|
|
|
|
// Pptx reads a PresentationML deck (.pptx): one section per slide, in the
|
|
// order the deck shows them, titled by the slide's title. A slide's text is
|
|
// its shapes in the order they are drawn — each shape's paragraphs as lines,
|
|
// a table as records — and then its speaker notes, under a "Notes:" line,
|
|
// since the notes are often where a deck says what its slides only list.
|
|
// Slide numbers, dates and footers repeat on every slide and say nothing
|
|
// about any of them, and are left out.
|
|
//
|
|
// The core properties come back as tags, and the deck's title is the first
|
|
// slide's title, falling back to the title in its properties.
|
|
type Pptx struct{}
|
|
|
|
// Parse reads every slide of the deck, and its notes.
|
|
func (Pptx) Parse(ctx context.Context, d semantic.Doc) (semantic.Doc, error) {
|
|
p, err := unzip(ctx, d.Text)
|
|
if err != nil {
|
|
return d, fmt.Errorf("parse pptx: %w", err)
|
|
}
|
|
main, core, err := begin(p)
|
|
if err != nil {
|
|
return d, fmt.Errorf("parse pptx: %w", err)
|
|
}
|
|
deck, err := part[struct {
|
|
Slides []struct {
|
|
Attr []xml.Attr `xml:",any,attr"`
|
|
} `xml:"sldIdLst>sldId"`
|
|
}](p, main)
|
|
if err != nil {
|
|
return d, fmt.Errorf("parse pptx: %w", err)
|
|
}
|
|
r, err := related(p, main)
|
|
if err != nil {
|
|
return d, fmt.Errorf("parse pptx: %w", err)
|
|
}
|
|
tags, err := props(p, core)
|
|
if err != nil {
|
|
return d, fmt.Errorf("parse pptx: %w", err)
|
|
}
|
|
|
|
s := show{p: p}
|
|
for i, sl := range deck.Slides {
|
|
name := r.id(rid(sl.Attr))
|
|
if name == "" {
|
|
return d, fmt.Errorf("parse pptx: slide %d has no part", i+1)
|
|
}
|
|
if err := s.slide(name); err != nil {
|
|
return d, fmt.Errorf("parse pptx: slide %d: %w", i+1, err)
|
|
}
|
|
}
|
|
s.b.trim()
|
|
|
|
kv := map[string]any{"format": "pptx"}
|
|
secs := tile(s.secs, s.b.len())
|
|
if len(secs) > 0 {
|
|
kv["sections"] = secs
|
|
}
|
|
if len(tags) > 0 {
|
|
kv["tags"] = tags
|
|
}
|
|
t := tags["title"]
|
|
for _, sec := range s.secs {
|
|
if sec.Title != "" {
|
|
t = sec.Title
|
|
break
|
|
}
|
|
}
|
|
if t != "" {
|
|
kv["title"] = t
|
|
}
|
|
return with(d, s.b.text(), kv), nil
|
|
}
|
|
|
|
// rid is the relationship id among an element's attributes: the one named
|
|
// "id" in a namespace, as opposed to the element's own unqualified id.
|
|
func rid(attr []xml.Attr) string {
|
|
for _, a := range attr {
|
|
if a.Name.Local == "id" && a.Name.Space != "" {
|
|
return a.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// show is a deck being laid out as text.
|
|
type show struct {
|
|
p *pkg
|
|
b buf
|
|
secs []Section
|
|
title string // the title of the slide being read
|
|
}
|
|
|
|
// slide writes one slide and its notes as a section.
|
|
func (s *show) slide(name string) error {
|
|
sl, err := part[elem](s.p, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := related(s.p, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
notes, err := part[elem](s.p, r.kind("notesSlide"))
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return err
|
|
}
|
|
|
|
s.b.gap()
|
|
start := s.b.len()
|
|
s.title = ""
|
|
if tree := sl.find("spTree"); tree != nil {
|
|
if err := s.shapes(tree); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if tree := notes.find("spTree"); tree != nil {
|
|
var said []string
|
|
for _, sp := range tree.kids {
|
|
if sp.name == "sp" && place(sp) == "body" {
|
|
said = append(said, paras(sp)...)
|
|
}
|
|
}
|
|
if len(said) > 0 {
|
|
s.b.gap()
|
|
s.b.raw("Notes:\n")
|
|
for _, l := range said {
|
|
s.b.put(l)
|
|
s.b.nl()
|
|
}
|
|
}
|
|
}
|
|
if s.b.len() > start {
|
|
s.secs = append(s.secs, Section{Title: s.title, Level: 1, Start: start})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// shapes writes the shapes of a tree in the order they are drawn, reading
|
|
// through groups and taking the first of alternative renderings.
|
|
func (s *show) shapes(tree *elem) error {
|
|
for _, k := range tree.kids {
|
|
var err error
|
|
switch k.name {
|
|
case "sp":
|
|
switch place(k) {
|
|
case "sldNum", "dt", "ftr", "hdr":
|
|
continue
|
|
case "title", "ctrTitle":
|
|
if s.title == "" {
|
|
s.title = k.said()
|
|
}
|
|
}
|
|
if ls := paras(k); len(ls) > 0 {
|
|
s.b.gap()
|
|
for _, l := range ls {
|
|
s.b.put(l)
|
|
s.b.nl()
|
|
}
|
|
}
|
|
case "grpSp":
|
|
err = s.shapes(k)
|
|
case "graphicFrame":
|
|
if t := k.find("tbl"); t != nil {
|
|
err = grid(s.p, &s.b, cells(t))
|
|
}
|
|
case "AlternateContent":
|
|
if len(k.kids) > 0 {
|
|
err = s.shapes(k.kids[0])
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// place is the kind of placeholder a shape fills — title, body, slide number
|
|
// — or "" for a shape drawn on the slide itself.
|
|
func place(sp *elem) string {
|
|
nv := sp.kid("nvSpPr")
|
|
if nv == nil {
|
|
return ""
|
|
}
|
|
if ph := nv.find("ph"); ph != nil {
|
|
if t := ph.at("type"); t != "" {
|
|
return t
|
|
}
|
|
return "obj" // the type ECMA-376 gives a placeholder that states none
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// paras is the text of a shape, a paragraph to a line, empty ones dropped.
|
|
func paras(sp *elem) []string {
|
|
body := sp.kid("txBody")
|
|
if body == nil {
|
|
return nil
|
|
}
|
|
var out []string
|
|
for _, p := range body.kids {
|
|
if p.name != "p" {
|
|
continue
|
|
}
|
|
if t := p.said(); t != "" {
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// cells reads a slide table's rows. Every column has a cell element, merged
|
|
// or not, so a cell's column is its position.
|
|
func cells(t *elem) [][]cell {
|
|
var rows [][]cell
|
|
for _, tr := range t.kids {
|
|
if tr.name != "tr" {
|
|
continue
|
|
}
|
|
var row []cell
|
|
col := 0
|
|
for _, tc := range tr.kids {
|
|
if tc.name != "tc" {
|
|
continue
|
|
}
|
|
if s := tc.said(); s != "" {
|
|
row = append(row, cell{col: col, text: s})
|
|
}
|
|
col++
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
return rows
|
|
}
|