PowerPoint text
The text of PowerPoint files: each slide's title, text and speaker notes in slide order, for search and imports.
Version 1.0.0 · 3 functions · 2 crates
Functions
What use pptx_text.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use pptx_slides, pptx_text from pptx_text (Quillo 0.6).
pptx_slides(path: str) → listor an errorEach slide as { number, title, text, notes }: title from its title placeholder ("" when none), text the rest of its text, a line per paragraph, notes the speaker notes. Hidden slides are included; pictures and charts give no text.
pptx_text(path: str) → stror an errorAll the text of a presentation, slide after slide, parted by a blank line; notes are left out.
pptx_info(path: str) → anyor an errorThe number of slides, and the title and author the file gives: { slides, title, creator }, null when missing.
Get it
$ quillo install pptx_text # pptx_text.qwer into this folder$ quillo install pptx_text --example # and its example, pptx_text.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/pptx_text.qwer$ curl -O https://quillolang.org/plugins/files/pptx_text.qwe$ curl -O https://quillolang.org/plugins/files/pptx_text_sample.pptxThen, in your program, all of it or only what you call:
use pptx_text.qweruse pptx_slides, pptx_text from pptx_text // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
zip = { version = "2", default-features = false, features = ["deflate"] }roxmltree = "0.20"Example
// A presentation's slides read for a search index and a page of its outline.//// quillo run pptx_text.qwe then open http://localhost:8080/slidesuse "pptx_text.qwer"let DECK = "pptx_text_sample.pptx"route GET /slides = { info: pptx_info(DECK)?, slides: pptx_slides(DECK)? }route GET /search ? { q: str = "coffee" } = pptx_slides(DECK)? | filter((s) => contains(lower(s.title + " " + s.text), lower(q))) | map((s) => { slide: s.number, title: s.title })route GET /outline = <html><body> <h1>{pptx_info(DECK)?.title}</h1> <ol>{for s in pptx_slides(DECK)? => <li>{s.title}</li>}</ol></body></html>$ quillo run pptx_text.qwe$ curl 'localhost:8080/slides'$ curl 'localhost:8080/search?q=coffee'$ curl 'localhost:8080/outline'Source
//! The text of PowerPoint files (.pptx): each slide's title, text and speaker notes, in slide order, for search and imports.//! version: 1.0.0//!//! [dependencies]//! zip = { version = "2", default-features = false, features = ["deflate"] }//! roxmltree = "0.20"use serde_json::{json, Value};use std::io::Read;const MAX_XML: u64 = 32 * 1024 * 1024;const MAX_SLIDES: usize = 5000;type Zip = zip::ZipArchive<std::fs::File>;fn open(path: &str) -> Result<Zip, String> { let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; zip::ZipArchive::new(f).map_err(|_| format!("{}: not a .pptx file (it is not a zip)", path))}fn read(z: &mut Zip, name: &str) -> Option<String> { let e = z.by_name(name).ok()?; let mut s = String::new(); e.take(MAX_XML).read_to_string(&mut s).ok()?; Some(s)}fn parse(xml: &str) -> Result<roxmltree::Document<'_>, String> { roxmltree::Document::parse_with_options(xml, roxmltree::ParsingOptions { allow_dtd: false, nodes_limit: 10_000_000 }).map_err(|e| e.to_string())}fn is(n: &roxmltree::Node, name: &str) -> bool { n.is_element() && n.tag_name().name() == name}fn attr<'a>(n: &roxmltree::Node<'a, '_>, name: &str) -> Option<&'a str> { n.attributes().find(|a| a.name() == name).map(|a| a.value())}// "slides/slide1.xml" seen from "ppt/presentation.xml" is "ppt/slides/slide1.xml"; "../notesSlides/n1.xml" from "ppt/slides/" is "ppt/notesSlides/n1.xml".fn resolve(dir: &str, target: &str) -> String { if let Some(t) = target.strip_prefix('/') { return t.to_string(); } let mut parts: Vec<&str> = dir.split('/').filter(|p| !p.is_empty()).collect(); for p in target.split('/') { match p { ".." => { parts.pop(); } "." | "" => {} p => parts.push(p), } } parts.join("/")}// Relationship id -> (type, target) of a part's .rels file.fn rels(z: &mut Zip, part: &str) -> Vec<(String, String, String)> { let (dir, file) = part.rsplit_once('/').unwrap_or(("", part)); let Some(xml) = read(z, &format!("{}/_rels/{}.rels", dir, file)) else { return vec![] }; let Ok(doc) = parse(&xml) else { return vec![] }; doc.descendants() .filter(|n| is(n, "Relationship")) .filter_map(|n| Some((attr(&n, "Id")?.to_string(), attr(&n, "Type")?.to_string(), resolve(dir, attr(&n, "Target")?)))) .collect()}// The slides' parts in the order the presentation shows them.fn slide_parts(z: &mut Zip) -> Result<Vec<String>, String> { let pres = read(z, "ppt/presentation.xml").ok_or("not a PowerPoint file (no ppt/presentation.xml)")?; let r = rels(z, "ppt/presentation.xml"); let doc = parse(&pres)?; let ids: Vec<String> = doc.descendants().filter(|n| is(n, "sldId")).filter_map(|n| n.attributes().find(|a| a.name() == "id" && a.namespace().is_some()).map(|a| a.value().to_string())).collect(); let mut parts: Vec<String> = ids.iter().filter_map(|id| r.iter().find(|(rid, _, _)| rid == id).map(|(_, _, t)| t.clone())).collect(); parts.truncate(MAX_SLIDES); Ok(parts)}fn paragraphs(n: &roxmltree::Node) -> Vec<String> { n.descendants() .filter(|p| is(p, "p") && p.tag_name().namespace().map_or(false, |ns| ns.contains("drawingml"))) .map(|p| { let mut s = String::new(); for d in p.descendants() { if is(&d, "t") { s.push_str(d.text().unwrap_or("")); } else if is(&d, "br") { s.push('\n'); } } s }) .filter(|s| !s.trim().is_empty()) .collect()}fn placeholder(sp: &roxmltree::Node) -> Option<String> { sp.descendants().find(|n| is(n, "ph")).map(|ph| attr(&ph, "type").unwrap_or("body").to_string())}struct Slide { title: String, text: Vec<String>, notes: String,}fn slide(z: &mut Zip, part: &str) -> Slide { let mut s = Slide { title: String::new(), text: vec![], notes: String::new() }; let Some(xml) = read(z, part) else { return s }; if let Ok(doc) = parse(&xml) { for sp in doc.descendants().filter(|n| is(n, "sp") || is(n, "graphicFrame")) { let ps = paragraphs(&sp); match placeholder(&sp).as_deref() { Some("title") | Some("ctrTitle") if s.title.is_empty() => s.title = ps.join(" "), Some("sldNum") | Some("dt") | Some("ftr") => {} _ => s.text.extend(ps), } } } if let Some((_, _, notes)) = rels(z, part).into_iter().find(|(_, t, _)| t.ends_with("/notesSlide")) { if let Some(xml) = read(z, ¬es) { if let Ok(doc) = parse(&xml) { let body: Vec<String> = doc.descendants().filter(|n| is(n, "sp") && placeholder(n).as_deref() == Some("body")).flat_map(|sp| paragraphs(&sp)).collect(); s.notes = body.join("\n"); } } } s}/// Each slide as `{ number, title, text, notes }`: `title` from its title placeholder ("" when none), `text` the rest of its text, a line per paragraph, `notes` the speaker notes./// Hidden slides are included; pictures and charts give no text.pub fn pptx_slides(path: &str) -> Result<Vec<Value>, String> { let mut z = open(path)?; let parts = slide_parts(&mut z)?; Ok(parts.iter().enumerate().map(|(i, p)| { let s = slide(&mut z, p); json!({ "number": i + 1, "title": s.title, "text": s.text.join("\n"), "notes": s.notes }) }).collect())}/// All the text of a presentation, slide after slide, parted by a blank line; notes are left out.pub fn pptx_text(path: &str) -> Result<String, String> { let mut z = open(path)?; let parts = slide_parts(&mut z)?; let mut out = vec![]; for p in &parts { let s = slide(&mut z, p); let mut lines = vec![]; if !s.title.is_empty() { lines.push(s.title); } lines.extend(s.text); out.push(lines.join("\n")); } Ok(out.join("\n\n").trim().to_string())}/// The number of slides, and the title and author the file gives: `{ slides, title, creator }`, null when missing.pub fn pptx_info(path: &str) -> Result<Value, String> { let mut z = open(path)?; let n = slide_parts(&mut z)?.len(); let mut v = json!({ "slides": n, "title": null, "creator": null }); if let Some(core) = read(&mut z, "docProps/core.xml") { if let Ok(doc) = parse(&core) { for key in ["title", "creator"] { if let Some(t) = doc.descendants().find(|n| is(n, key)).and_then(|n| n.text()) { v[key] = Value::String(t.trim().to_string()); } } } } Ok(v)}Related
odf_text.qwer · 1.0.0The text of LibreOffice files (.odt, .ods, .odp): paragraphs with headings and lists, sheets as rows of typed cells, slides with their notes, and the title and author.7 views · 9 downloadsPDF text pdf_text.qwer · 1.0.0The text of a PDF, whole or by page, and the lines holding a word: for search, imports and reading invoices.7 views · 6 downloadsWord text docx_text.qwer · 1.0.0The text of Word documents: all of it, paragraph by paragraph with their styles, the tables as rows, and the title and author.7 views · 9 downloadsRTF rtf.qwer · 1.0.0RTF documents read as plain text with their title and author, and simple RTF written: headings, bold and italic runs and tables, for Word, Pages and TextEdit.7 views · 8 downloadsText encodings text_encoding.qwer · 1.0.0Text files in old encodings (Windows-1252, Latin-1, Shift_JIS, GBK, Big5...) detected and read as UTF-8, and written back in them.8 views · 8 downloads7-Zip sevenz.qwer · 1.0.07-Zip archives listed, one file read, opened safely into a folder, and made from a folder.7 views · 8 downloadsThread 0 posts
On PowerPoint text: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.