mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: pres artifact 3 (#13346)
This commit is contained in:
committed by
GitHub
Unverified
parent
72dc444b2c
commit
564a883c2a
@@ -37,6 +37,7 @@ use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use zip::ZipArchive;
|
||||
@@ -1326,14 +1327,27 @@ impl PresentationArtifactManager {
|
||||
let args: AddTableArgs = parse_args(&request.action, &request.args)?;
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let document = self.get_document_mut(&artifact_id, &request.action)?;
|
||||
let rows = coerce_table_rows(args.rows, &request.action)?;
|
||||
let mut frame: Rect = args.position.into();
|
||||
let (column_widths, row_heights) = normalize_table_dimensions(
|
||||
&rows,
|
||||
frame,
|
||||
args.column_widths,
|
||||
args.row_heights,
|
||||
&request.action,
|
||||
)?;
|
||||
frame.width = column_widths.iter().sum();
|
||||
frame.height = row_heights.iter().sum();
|
||||
let element_id = document.next_element_id();
|
||||
let slide = document.get_slide_mut(args.slide_index, &request.action)?;
|
||||
slide
|
||||
.elements
|
||||
.push(PresentationElement::Table(TableElement {
|
||||
element_id: element_id.clone(),
|
||||
frame: args.position.into(),
|
||||
rows: coerce_table_rows(args.rows, &request.action)?,
|
||||
frame,
|
||||
rows,
|
||||
column_widths,
|
||||
row_heights,
|
||||
style: args.style,
|
||||
merges: Vec::new(),
|
||||
z_order: slide.elements.len(),
|
||||
@@ -2437,6 +2451,18 @@ impl PresentationDocument {
|
||||
.collect()
|
||||
})
|
||||
.collect(),
|
||||
column_widths: imported_table
|
||||
.column_widths
|
||||
.iter()
|
||||
.copied()
|
||||
.map(emu_to_points)
|
||||
.collect(),
|
||||
row_heights: imported_table
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| row.height.map_or(400_000, |height| height))
|
||||
.map(emu_to_points)
|
||||
.collect(),
|
||||
style: None,
|
||||
merges: Vec::new(),
|
||||
z_order: slide.elements.len(),
|
||||
@@ -2821,15 +2847,18 @@ impl PresentationSlide {
|
||||
}
|
||||
}
|
||||
PresentationElement::Table(table) => {
|
||||
let row_count = table.rows.len().max(1) as u32;
|
||||
let column_count =
|
||||
table.rows.iter().map(std::vec::Vec::len).max().unwrap_or(1) as u32;
|
||||
let column_width = points_to_emu(table.frame.width / column_count.max(1));
|
||||
let mut builder = TableBuilder::new(vec![column_width; column_count as usize])
|
||||
.position(
|
||||
points_to_emu(table.frame.left),
|
||||
points_to_emu(table.frame.top),
|
||||
);
|
||||
let mut builder = TableBuilder::new(
|
||||
table
|
||||
.column_widths
|
||||
.iter()
|
||||
.copied()
|
||||
.map(points_to_emu)
|
||||
.collect(),
|
||||
)
|
||||
.position(
|
||||
points_to_emu(table.frame.left),
|
||||
points_to_emu(table.frame.top),
|
||||
);
|
||||
for (row_index, row) in table.rows.into_iter().enumerate() {
|
||||
let cells = row
|
||||
.into_iter()
|
||||
@@ -2838,9 +2867,12 @@ impl PresentationSlide {
|
||||
build_table_cell(cell, &table.merges, row_index, column_index)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
builder = builder.add_row(TableRow::new(cells));
|
||||
let mut table_row = TableRow::new(cells);
|
||||
if let Some(height) = table.row_heights.get(row_index) {
|
||||
table_row = table_row.with_height(points_to_emu(*height));
|
||||
}
|
||||
builder = builder.add_row(table_row);
|
||||
}
|
||||
let _ = row_count;
|
||||
content = content.table(builder.build());
|
||||
}
|
||||
PresentationElement::Chart(chart) => {
|
||||
@@ -2993,6 +3025,8 @@ struct TableElement {
|
||||
element_id: String,
|
||||
frame: Rect,
|
||||
rows: Vec<Vec<TableCellSpec>>,
|
||||
column_widths: Vec<u32>,
|
||||
row_heights: Vec<u32>,
|
||||
style: Option<String>,
|
||||
merges: Vec<TableMergeRegion>,
|
||||
z_order: usize,
|
||||
@@ -3609,6 +3643,8 @@ struct AddTableArgs {
|
||||
slide_index: u32,
|
||||
position: PositionArgs,
|
||||
rows: Vec<Vec<Value>>,
|
||||
column_widths: Option<Vec<u32>>,
|
||||
row_heights: Option<Vec<u32>>,
|
||||
style: Option<String>,
|
||||
}
|
||||
|
||||
@@ -4099,6 +4135,57 @@ fn coerce_table_rows(
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn normalize_table_dimensions(
|
||||
rows: &[Vec<TableCellSpec>],
|
||||
frame: Rect,
|
||||
column_widths: Option<Vec<u32>>,
|
||||
row_heights: Option<Vec<u32>>,
|
||||
action: &str,
|
||||
) -> Result<(Vec<u32>, Vec<u32>), PresentationArtifactError> {
|
||||
let column_count = rows.iter().map(std::vec::Vec::len).max().unwrap_or(1);
|
||||
let normalized_column_widths = match column_widths {
|
||||
Some(widths) => {
|
||||
if widths.len() != column_count {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!(
|
||||
"`column_widths` must contain {column_count} entries for this table"
|
||||
),
|
||||
});
|
||||
}
|
||||
widths
|
||||
}
|
||||
None => split_points(frame.width, column_count),
|
||||
};
|
||||
let normalized_row_heights = match row_heights {
|
||||
Some(heights) => {
|
||||
if heights.len() != rows.len() {
|
||||
return Err(PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!(
|
||||
"`row_heights` must contain {} entries for this table",
|
||||
rows.len()
|
||||
),
|
||||
});
|
||||
}
|
||||
heights
|
||||
}
|
||||
None => split_points(frame.height, rows.len()),
|
||||
};
|
||||
Ok((normalized_column_widths, normalized_row_heights))
|
||||
}
|
||||
|
||||
fn split_points(total: u32, count: usize) -> Vec<u32> {
|
||||
if count == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let base = total / count as u32;
|
||||
let remainder = total % count as u32;
|
||||
(0..count)
|
||||
.map(|index| base + u32::from(index < remainder as usize))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_alignment(value: &str, action: &str) -> Result<TextAlignment, PresentationArtifactError> {
|
||||
match value {
|
||||
"left" => Ok(TextAlignment::Left),
|
||||
@@ -4129,13 +4216,24 @@ fn normalize_theme(args: ThemeArgs, action: &str) -> Result<ThemeState, Presenta
|
||||
}
|
||||
|
||||
fn parse_slide_size(value: &Value, action: &str) -> Result<Rect, PresentationArtifactError> {
|
||||
let position: PositionArgs = serde_json::from_value(value.clone()).map_err(|error| {
|
||||
#[derive(Deserialize)]
|
||||
struct SlideSizeArgs {
|
||||
width: u32,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
let slide_size: SlideSizeArgs = serde_json::from_value(value.clone()).map_err(|error| {
|
||||
PresentationArtifactError::InvalidArgs {
|
||||
action: action.to_string(),
|
||||
message: format!("invalid slide_size: {error}"),
|
||||
}
|
||||
})?;
|
||||
Ok(position.into())
|
||||
Ok(Rect {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: slide_size.width,
|
||||
height: slide_size.height,
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_layout_to_slide(
|
||||
@@ -4516,6 +4614,8 @@ fn inspect_document(
|
||||
"slide": index + 1,
|
||||
"rows": table.rows.len(),
|
||||
"cols": table.rows.iter().map(std::vec::Vec::len).max().unwrap_or(0),
|
||||
"columnWidths": table.column_widths,
|
||||
"rowHeights": table.row_heights,
|
||||
"preview": table.rows.first().map(|row| row.iter().map(|cell| cell.text.clone()).collect::<Vec<_>>().join(" | ")),
|
||||
"style": table.style,
|
||||
"bbox": [table.frame.left, table.frame.top, table.frame.width, table.frame.height],
|
||||
@@ -4744,6 +4844,8 @@ fn resolve_anchor(
|
||||
"slideIndex": slide_index,
|
||||
"rows": table.rows.len(),
|
||||
"cols": table.rows.iter().map(std::vec::Vec::len).max().unwrap_or(0),
|
||||
"columnWidths": table.column_widths,
|
||||
"rowHeights": table.row_heights,
|
||||
"bbox": [table.frame.left, table.frame.top, table.frame.width, table.frame.height],
|
||||
"bboxUnit": "points",
|
||||
}),
|
||||
@@ -4804,10 +4906,10 @@ fn build_pptx_bytes(document: &PresentationDocument, action: &str) -> Result<Vec
|
||||
.to_ppt_rs()
|
||||
.build()
|
||||
.map_err(|error| format!("{action}: {error}"))?;
|
||||
patch_pptx_hyperlinks(bytes, document).map_err(|error| format!("{action}: {error}"))
|
||||
patch_pptx_package(bytes, document).map_err(|error| format!("{action}: {error}"))
|
||||
}
|
||||
|
||||
fn patch_pptx_hyperlinks(
|
||||
fn patch_pptx_package(
|
||||
source_bytes: Vec<u8>,
|
||||
document: &PresentationDocument,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
@@ -4837,6 +4939,20 @@ fn patch_pptx_hyperlinks(
|
||||
writer
|
||||
.start_file(&name, options)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if name == "ppt/presentation.xml" {
|
||||
writer
|
||||
.write_all(
|
||||
update_presentation_xml_dimensions(bytes, document.slide_size)?.as_bytes(),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
continue;
|
||||
}
|
||||
if let Some(slide_number) = parse_slide_xml_path(&name) {
|
||||
writer
|
||||
.write_all(update_slide_xml(bytes, &document.slides[slide_number - 1])?.as_bytes())
|
||||
.map_err(|error| error.to_string())?;
|
||||
continue;
|
||||
}
|
||||
if let Some(slide_number) = parse_slide_relationships_path(&name)
|
||||
&& let Some(relationships) = pending_slide_relationships.remove(&slide_number)
|
||||
{
|
||||
@@ -4868,6 +4984,42 @@ fn patch_pptx_hyperlinks(
|
||||
.map(Cursor::into_inner)
|
||||
}
|
||||
|
||||
fn update_presentation_xml_dimensions(
|
||||
existing_bytes: Vec<u8>,
|
||||
slide_size: Rect,
|
||||
) -> Result<String, String> {
|
||||
let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?;
|
||||
let updated = replace_self_closing_xml_tag(
|
||||
&existing,
|
||||
"p:sldSz",
|
||||
&format!(
|
||||
r#"<p:sldSz cx="{}" cy="{}" type="screen4x3"/>"#,
|
||||
points_to_emu(slide_size.width),
|
||||
points_to_emu(slide_size.height)
|
||||
),
|
||||
)?;
|
||||
replace_self_closing_xml_tag(
|
||||
&updated,
|
||||
"p:notesSz",
|
||||
&format!(
|
||||
r#"<p:notesSz cx="{}" cy="{}"/>"#,
|
||||
points_to_emu(slide_size.height),
|
||||
points_to_emu(slide_size.width)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn replace_self_closing_xml_tag(xml: &str, tag: &str, replacement: &str) -> Result<String, String> {
|
||||
let start = xml
|
||||
.find(&format!("<{tag} "))
|
||||
.ok_or_else(|| format!("presentation xml is missing `<{tag} .../>`"))?;
|
||||
let end = xml[start..]
|
||||
.find("/>")
|
||||
.map(|offset| start + offset + 2)
|
||||
.ok_or_else(|| format!("presentation xml tag `{tag}` is not self-closing"))?;
|
||||
Ok(format!("{}{replacement}{}", &xml[..start], &xml[end..]))
|
||||
}
|
||||
|
||||
fn slide_hyperlink_relationships(slide: &PresentationSlide) -> Vec<String> {
|
||||
let mut ordered = slide.elements.iter().collect::<Vec<_>>();
|
||||
ordered.sort_by_key(|element| element.z_order());
|
||||
@@ -4898,6 +5050,13 @@ fn parse_slide_relationships_path(path: &str) -> Option<usize> {
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn parse_slide_xml_path(path: &str) -> Option<usize> {
|
||||
path.strip_prefix("ppt/slides/slide")?
|
||||
.strip_suffix(".xml")?
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn update_slide_relationships_xml(
|
||||
existing_bytes: Vec<u8>,
|
||||
relationships: &[String],
|
||||
@@ -4922,6 +5081,68 @@ fn slide_relationships_xml(relationships: &[String]) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn update_slide_xml(existing_bytes: Vec<u8>, slide: &PresentationSlide) -> Result<String, String> {
|
||||
let existing = String::from_utf8(existing_bytes).map_err(|error| error.to_string())?;
|
||||
let table_xml = slide_table_xml(slide);
|
||||
if table_xml.is_empty() {
|
||||
return Ok(existing);
|
||||
}
|
||||
existing
|
||||
.contains("</p:spTree>")
|
||||
.then(|| existing.replace("</p:spTree>", &format!("{table_xml}\n</p:spTree>")))
|
||||
.ok_or_else(|| "slide xml is missing a closing `</p:spTree>`".to_string())
|
||||
}
|
||||
|
||||
fn slide_table_xml(slide: &PresentationSlide) -> String {
|
||||
let mut ordered = slide.elements.iter().collect::<Vec<_>>();
|
||||
ordered.sort_by_key(|element| element.z_order());
|
||||
let mut table_index = 0_usize;
|
||||
ordered
|
||||
.into_iter()
|
||||
.filter_map(|element| {
|
||||
let PresentationElement::Table(table) = element else {
|
||||
return None;
|
||||
};
|
||||
table_index += 1;
|
||||
let rows = table
|
||||
.rows
|
||||
.clone()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(row_index, row)| {
|
||||
let cells = row
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(column_index, cell)| {
|
||||
build_table_cell(cell, &table.merges, row_index, column_index)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut table_row = TableRow::new(cells);
|
||||
if let Some(height) = table.row_heights.get(row_index) {
|
||||
table_row = table_row.with_height(points_to_emu(*height));
|
||||
}
|
||||
Some(table_row)
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(ppt_rs::generator::table::generate_table_xml(
|
||||
&ppt_rs::generator::table::Table::new(
|
||||
rows,
|
||||
table
|
||||
.column_widths
|
||||
.iter()
|
||||
.copied()
|
||||
.map(points_to_emu)
|
||||
.collect(),
|
||||
points_to_emu(table.frame.left),
|
||||
points_to_emu(table.frame.top),
|
||||
),
|
||||
300 + table_index,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn write_preview_images(
|
||||
document: &PresentationDocument,
|
||||
output_dir: &Path,
|
||||
@@ -4938,13 +5159,74 @@ fn write_preview_images(
|
||||
path: pptx_path.clone(),
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
document
|
||||
.to_ppt_rs()
|
||||
.save_as_png(output_dir)
|
||||
render_pptx_to_pngs(&pptx_path, output_dir, action)
|
||||
}
|
||||
|
||||
fn render_pptx_to_pngs(
|
||||
pptx_path: &Path,
|
||||
output_dir: &Path,
|
||||
action: &str,
|
||||
) -> Result<(), PresentationArtifactError> {
|
||||
let soffice_cmd = if cfg!(target_os = "macos")
|
||||
&& Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists()
|
||||
{
|
||||
"/Applications/LibreOffice.app/Contents/MacOS/soffice"
|
||||
} else {
|
||||
"soffice"
|
||||
};
|
||||
let conversion = Command::new(soffice_cmd)
|
||||
.arg("--headless")
|
||||
.arg("--convert-to")
|
||||
.arg("pdf")
|
||||
.arg(pptx_path)
|
||||
.arg("--outdir")
|
||||
.arg(output_dir)
|
||||
.output()
|
||||
.map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.to_path_buf(),
|
||||
message: format!("{action}: failed to execute LibreOffice: {error}"),
|
||||
})?;
|
||||
if !conversion.status.success() {
|
||||
return Err(PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.to_path_buf(),
|
||||
message: format!(
|
||||
"{action}: LibreOffice conversion failed: {}",
|
||||
String::from_utf8_lossy(&conversion.stderr)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let pdf_path = output_dir.join(
|
||||
pptx_path
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.map(|stem| format!("{stem}.pdf"))
|
||||
.ok_or_else(|| PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.to_path_buf(),
|
||||
message: format!("{action}: preview pptx filename is invalid"),
|
||||
})?,
|
||||
);
|
||||
let prefix = output_dir.join("slide");
|
||||
let conversion = Command::new("pdftoppm")
|
||||
.arg("-png")
|
||||
.arg(&pdf_path)
|
||||
.arg(&prefix)
|
||||
.output()
|
||||
.map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: pdf_path.clone(),
|
||||
message: format!("{action}: failed to execute pdftoppm: {error}"),
|
||||
})?;
|
||||
std::fs::remove_file(&pdf_path).ok();
|
||||
if !conversion.status.success() {
|
||||
return Err(PresentationArtifactError::ExportFailed {
|
||||
path: output_dir.to_path_buf(),
|
||||
message: format!("{action}: {error}"),
|
||||
})
|
||||
message: format!(
|
||||
"{action}: pdftoppm conversion failed: {}",
|
||||
String::from_utf8_lossy(&conversion.stderr)
|
||||
),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_preview_image(
|
||||
|
||||
@@ -133,6 +133,48 @@ fn manager_can_import_exported_presentation() -> Result<(), Box<dyn std::error::
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_slide_size_is_written_to_exported_pptx() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({
|
||||
"name": "Custom Size",
|
||||
"slide_size": { "width": 960, "height": 540 }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(created.artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let export_path = temp_dir.path().join("custom-size.pptx");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(created.artifact_id),
|
||||
action: "export_pptx".to_string(),
|
||||
args: serde_json::json!({ "path": export_path }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let presentation_xml = zip_entry_text(
|
||||
&temp_dir.path().join("custom-size.pptx"),
|
||||
"ppt/presentation.xml",
|
||||
)?;
|
||||
assert!(presentation_xml.contains(r#"cx="12192000" cy="6858000""#));
|
||||
assert!(presentation_xml.contains(r#"p:notesSz cx="6858000" cy="12192000""#));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_fit_contain_preserves_aspect_ratio() {
|
||||
let image = ImageElement {
|
||||
@@ -1288,6 +1330,8 @@ fn manager_supports_table_cell_updates_and_merges() -> Result<(), Box<dyn std::e
|
||||
"slide_index": 0,
|
||||
"position": { "left": 24, "top": 24, "width": 240, "height": 120 },
|
||||
"rows": [["A", "B"], ["C", "D"]],
|
||||
"column_widths": [90, 150],
|
||||
"row_heights": [40, 80],
|
||||
"style": "TableStyleMedium9"
|
||||
}),
|
||||
},
|
||||
@@ -1331,6 +1375,38 @@ fn manager_supports_table_cell_updates_and_merges() -> Result<(), Box<dyn std::e
|
||||
.expect("inspect")
|
||||
.contains("\"kind\":\"table\"")
|
||||
);
|
||||
let resolved = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "resolve".to_string(),
|
||||
args: serde_json::json!({ "id": format!("tb/{table_id}") }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
assert_eq!(
|
||||
resolved
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("columnWidths"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|widths| widths
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_u64)
|
||||
.collect::<Vec<_>>()),
|
||||
Some(vec![90, 150])
|
||||
);
|
||||
assert_eq!(
|
||||
resolved
|
||||
.resolved_record
|
||||
.as_ref()
|
||||
.and_then(|record| record.get("rowHeights"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|heights| heights
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_u64)
|
||||
.collect::<Vec<_>>()),
|
||||
Some(vec![40, 80])
|
||||
);
|
||||
|
||||
let merged = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
@@ -1353,5 +1429,25 @@ fn manager_supports_table_cell_updates_and_merges() -> Result<(), Box<dyn std::e
|
||||
.map(|snapshot| snapshot.slide_count),
|
||||
Some(1)
|
||||
);
|
||||
let export_path = temp_dir.path().join("tables.pptx");
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(merged.artifact_id),
|
||||
action: "export_pptx".to_string(),
|
||||
args: serde_json::json!({ "path": export_path }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let slide_xml = zip_entry_text(
|
||||
&temp_dir.path().join("tables.pptx"),
|
||||
"ppt/slides/slide1.xml",
|
||||
)?;
|
||||
assert!(
|
||||
slide_xml.contains(r#"<a:gridCol w="1143000"/>"#),
|
||||
"{slide_xml}"
|
||||
);
|
||||
assert!(slide_xml.contains(r#"<a:gridCol w="1905000"/>"#));
|
||||
assert!(slide_xml.contains(r#"<a:tr h="508000">"#));
|
||||
assert!(slide_xml.contains(r#"<a:tr h="1016000">"#));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -55,9 +55,14 @@ Supported actions:
|
||||
Example create:
|
||||
`{"action":"create","args":{"name":"Quarterly Update"}}`
|
||||
|
||||
Example create with custom slide size:
|
||||
`{"action":"create","args":{"name":"Quarterly Update","slide_size":{"width":960,"height":540}}}`
|
||||
|
||||
Example edit:
|
||||
`{"artifact_id":"presentation_x","action":"add_text_shape","args":{"slide_index":0,"text":"Revenue up 24%","position":{"left":48,"top":72,"width":260,"height":80}}}`
|
||||
|
||||
Table creation also accepts optional `column_widths` and `row_heights` arrays in points when you need explicit table sizing instead of even splits.
|
||||
|
||||
Example export:
|
||||
`{"artifact_id":"presentation_x","action":"export_pptx","args":{"path":"artifacts/q2-update.pptx"}}`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user