three.js로 입체 모델을 만들고 회전 가능한 뷰어에서 모든 각도로 확인합니다. OBJ + MTL이나 GLB로 저장해 다른 3D 제작 소프트웨어에서 사용할 수 있습니다.
- 입체 모델을 만들고 모든 각도에서 확인하기
- Blender에서 편집할 모델 만들기
- 모델의 윤곽과 재질 표현 확인하기
개요나 문서로 PowerPoint를 새로 만들고 기존 슬라이드를 편집하거나 디자인을 바꿀 수 있습니다. 슬라이드의 텍스트와 구조를 추출하고 PDF 또는 페이지별 이미지로 내보내며, 명확한 메시지와 읽기 쉬운 레이아웃 및 결과 점검을 안내합니다.
Use this skill any time a presentation file is the primary input or output. Covers: creating new .pptx decks from scratch or from an outline/document (pptxgenjs / python-pptx); editing and restyling an existing .pptx; reading and extracting text/structure from a deck (markitdown); and rendering a deck to PDF (LibreOffice) or to per-slide PNG images (pdftoppm) for previews, thumbnails, or visual review. Trigger when the user references a .pptx file by name or path, says 'make/build a deck, slides, presentation, PPT, 幻灯片, 演示文稿, 汇报/路演材料', or asks to convert, export, render, or preview slides as PDF or images (e.g. 'PPT转PDF', 'ppt导出图片', 'turn these slides into a PDF').
실행 점수 미검증
설치 방법 보기이 스킬은 PowerPoint(.pptx) 작업을 지원합니다. 개요나 문서에서 새 슬라이드를 만들고, 기존 자료를 편집하거나 디자인을 바꾸며, 슬라이드의 텍스트와 구조를 추출하고, PDF 또는 페이지별 PNG 이미지로 내보낼 수 있습니다. 각 슬라이드에 분명한 주장을 담고 도표나 이미지로 내용을 전달하는 구성, 색과 글자의 가독성, 여백과 정보량 조절을 안내합니다. 제작이나 편집이 끝나면 빠진 내용과 글자 넘침을 확인하고, 필요한 경우 렌더링한 이미지로 화면을 살펴보는 절차도 다룹니다. 기존 파일은 보통 원래 위치에 보존하고 업데이트본을 별도 파일로 만듭니다.
회의, 수업, 고객 제안에 쓸 슬라이드를 메모나 문서에서 만들 때 활용할 수 있습니다. 기존 프레젠테이션의 모양을 다듬거나, 내용을 추출해 재사용하거나, PDF와 이미지로 공유하고 확인할 때도 유용합니다. 자료에는 pptxgenjs와 python-pptx를 사용해 만드는 방법도 포함되어 있습니다.
.pptx의 내용을 추출하고 PDF와 슬라이드 이미지를 만들어 주세요.”자료에 나온 PDF 내보내기에는 LibreOffice를, 슬라이드 이미지 변환에는 pdftoppm을 사용합니다. 새로 만들거나 편집할 때는 pptxgenjs와 python-pptx, 내용을 추출할 때는 markitdown이 언급됩니다. 라이선스는 Proprietary이며 자세한 내용은 LICENSE.txt에 있습니다.
이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.
AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.
점수에는 넣지 않았습니다. 2026년 10월 24일 기준으로 AI(OpenAI Codex)가 판정했습니다. 참고용이므로 설치 전에 원문도 확인해 주세요.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
In one sentence: don't make boring slides. Bullet points on a white background are forgettable.
But "not boring" is not "flashy." The goal is a deck that looks ready to use in real life — the kind you could drop into a real meeting, class, or client pitch without editing.
Substance comes before styling: every slide must carry one specific, concrete claim — real numbers, names and mechanisms, and the "so what" behind them — not a topic label or generic filler. Depth is not density: it comes from picking the sharpest fact and cutting the rest, never from more text.
Original file preserved: user-provided decks are read-only by default. Deliverables go to new files
(<stem>_updated.pptx next to the input); edit an original in place only when the user explicitly
asks — and then copy it to <stem>_backup.pptx next to it (never /tmp) first. See "Editing an
existing PowerPoint presentation" in Part 2.
① Pick a content-informed color palette
The palette should feel designed for this topic. A good test: if you could drop your colors into a completely unrelated deck and it would still "work," your choices aren't specific enough. Derive the hues from the subject itself — its place, industry, brand, or mood — rather than reaching for a default.
② Dominance, not equality
One color should dominate 60–70% of the visual weight, supported by 1–2 secondary tones and one sharp accent. Never give all colors equal weight.
③ Dark/light contrast + a visual motif
Build the palette on the BACKGROUND → PRIMARY → ACCENT model, and reuse those exact same three roles on every single slide:
BG, PRIMARY, ACCENT, plus TEXT and MUTED) and reference only those constants — never hand-pick ad-hoc colors slide by slide.Every slide needs a visual element — image, chart, diagram, or shape. Text-only slides are forgettable.
Every slide needs ONE clear visual focal point — an oversized number, a chart, an image, a single bold statement — that dominates the composition. Everything else is subordinate to it.
Prefer container-free grouping — use it on the majority of slides. Grouping comes from alignment, spacing and weight, not from drawing a box around things:
Pick layouts that serve each slide's purpose:
Data display: (point sizes here and throughout assume the 13.33 × 7.5" canvas — see §6)
Density & rhythm:
addChart / add_chart with CategoryChartData, or composed shapes + connectors + text boxes) so they inherit the palette and cost zero image calls.sizing: { type: 'cover' } or an explicit crop), never distort it by setting a w/h that breaks the ratio. This matters most on covers and full-bleed backgrounds, where a stretched photo is the most visible defect on the deck.run.font.name in python-pptx, fontFace in pptxgenjs) — do not rely on inherited theme fonts.Preview caveat: the font names you write into the
.pptxare rendered by the user's PowerPoint, not by your build environment. If you preview via LibreOffice, it substitutes any font it doesn't have — and substitutes with different character widths make the preview's "overflow / fits" verdict disagree with the real deck. Prefer faces that both ship with Office and render true-to-width locally; where you can't, leave ~10% slack instead of trusting the preview.
margin to 0 (or offset the shape to compensate)FFFFFF or your brand color; avoid warm-neutral defaults like F5F5DC, FAF0E6, FAEBD7, FFF8E1Content QA: check for missing content, typos, wrong order; when using a template, grep for leftover placeholders (xxx, lorem, TODO, [insert, etc.).
Code QA: run a short python-pptx script over the finished deck to flag text overflow (estimated text height/width vs. the shape box, plus boxes outside the slide) and overlap (bounding-box intersection between two text-bearing shapes), then fix the real hits and re-run.
Visual QA once only: render the deck to images and use the visual-judge subagent (if not present, check it yourself) when necessary. Do not call an external VLM to check slides for visual QA.
Output QA (before handing the deck over): the user's original input file is untouched at its original path (unless the user explicitly asked for in-place editing); any backup you created stays next to it — these are NOT temp/retry artifacts. The deliverable is a new file (<stem>_updated.pptx), and intermediate renders (the PDF and per-slide PNGs from the commands below) are cleaned up.
PPTX → images is two steps — pdftoppm reads PDF, not .pptx, so LibreOffice must convert first:
# Step 1: PPTX → PDF (LibreOffice; also the command for any "export deck as PDF" request)
soffice --headless --convert-to pdf --outdir <out_dir> deck.pptx
# Step 2: PDF → per-slide PNGs
pdftoppm -png -r 150 <out_dir>/deck.pdf <out_dir>/slide
# Generates slide-1.png, slide-2.png, ...
If
sofficeis only available under a full path (macOS:
/Applications/LibreOffice.app/Contents/MacOS/soffice), call it there. Only one LibreOffice
instance may run at a time — if a conversion hangs, add
--env:UserInstallation=file:///tmp/libreoffice_tmp.🔴
sofficenot installed? You MUST install it, not substitute it — using PowerPoint/Keynote/WPS
to render instead is forbidden, and a large download is not a reason to skip. Once installed,
register it onPATHand re-verifysoffice --versionso the next probe doesn't reinstall it
(step 4 of the hard requirement). See
HARD REQUIREMENT below.
pptxgenjs generates .pptx files in JavaScript / Node.js. Coordinates are in inches.
npm install -g pptxgenjs
const pptxgen = require("pptxgenjs");
let pres = new pptxgen();
pres.layout = 'LAYOUT_WIDE'; // 13.33 × 7.5" — the default for this skill (see below)
pres.author = 'Your Name';
pres.title = 'Presentation Title';
// Canvas constants — derive every position from these, never hardcode 10 or 13.33
const W = 13.33, H = 7.5, M = 0.5; // width, height, margin (true width is 13.333"; 13.33 keeps you inside it)
let slide = pres.addSlide();
slide.addText("Hello World!", { x: M, y: M, w: W - 2 * M, fontSize: 36, color: "363636" });
pres.writeFile({ fileName: "Presentation.pptx" }).then(() => console.log("done"));
| Layout | Size (inches) |
|---|---|
LAYOUT_WIDE | 13.33 × 7.5 — use this |
LAYOUT_16x9 | 10 × 5.625 (pptxgenjs default) |
LAYOUT_16x10 | 10 × 6.25 |
LAYOUT_4x3 | 10 × 7.5 |
⚠️ Set
LAYOUT_WIDEexplicitly on every deck. pptxgenjs defaults toLAYOUT_16x9, which is only 10 × 5.625" — but every type size in this skill (§6: 44–72pt titles, ~24pt body; §3: 60–72pt stat callouts) is tuned for a 13.33 × 7.5" canvas. Leaving the default gives you a canvas 25% narrower and 25% shorter with type sized for the big one, which overflows systematically — the exact failure §3, §9 and §10 all tell you to avoid. If you deliberately target a 10"-wide layout, scale every font size and the 0.5" margins down by the same factor (×0.75).
// Basic text
slide.addText("Simple Text", {
x: 1, y: 1, w: 8, h: 2, fontSize: 24, fontFace: "Arial",
color: "363636", bold: true, align: "center", valign: "middle"
});
// Character spacing: use charSpacing (letterSpacing is silently ignored)
slide.addText("SPACED TEXT", { x: 1, y: 1, w: 8, h: 1, charSpacing: 6 });
// Rich text array (mixed styles in one paragraph)
slide.addText([
{ text: "Bold ", options: { bold: true } },
{ text: "Italic ", options: { italic: true } }
], { x: 1, y: 3, w: 8, h: 1 });
// Multi-line (each line needs breakLine: true; the last may omit it)
slide.addText([
{ text: "Line 1", options: { breakLine: true } },
{ text: "Line 2", options: { breakLine: true } },
{ text: "Line 3" }
], { x: 0.5, y: 0.5, w: 8, h: 2 });
// Text-box padding: set margin: 0 to align with shapes/lines
slide.addText("Title", { x: 0.5, y: 0.3, w: 9, h: 0.6, margin: 0 });
⚠️ Rich text arrays emit one
<a:pPr>per run, not per paragraph. Two or more consecutive items withoutbreakLineland in the same<a:p>, each carrying its own<a:pPr>— which violates thepPr? (r|br|fld)* endParaRPr?schema. LibreOffice renders it fine, so a PDF preview will not catch it; PowerPoint paints the first frame correctly, then re-lays-out the paragraph and the line garbles. Two safe options: give every itembreakLine: true(one run per paragraph), or, when you genuinely need mixed formatting inline, post-process the slide XML afterwriteFile()and drop every<a:pPr>after the first one inside each<a:p>. Do not fix it by splitting the runs into separate paragraphs — that silently turns one inline-mixed line into two lines and changes the layout you designed.
// ✅ Correct: multiple bullets
slide.addText([
{ text: "First item", options: { bullet: true, breakLine: true } },
{ text: "Second item", options: { bullet: true, breakLine: true } },
{ text: "Third item", options: { bullet: true } }
], { x: 0.5, y: 0.5, w: 8, h: 3 });
// ❌ Wrong: never use unicode bullets (creates double bullets)
slide.addText("• First item", { ... });
// Sub-items & numbered lists
{ text: "Sub-item", options: { bullet: true, indentLevel: 1 } }
{ text: "First", options: { bullet: { type: "number" }, breakLine: true } }
bullet: true looks amateurish)The bare bullet: true renders a big dot with a huge gap to the text (pptxgenjs defaults to a ~27pt hanging indent) — a classic AI-list tell. Always style bullets:
// bullet is a PARAGRAPH property — put it in EACH item's options, not top-level.
// A top-level `bullet` only styles the first paragraph; the rest get <a:buNone/> (no dot).
const bu = () => ({ code: "2022", indent: 14 }); // factory: fresh object per item (pptxgenjs mutates in place)
slide.addText([
{ text: "First item", options: { bullet: bu(), breakLine: true } },
{ text: "Second item", options: { bullet: bu(), breakLine: true } },
{ text: "Third item", options: { bullet: bu() } }
], {
x: 0.5, y: 0.5, w: 8, h: 3, fontSize: 15, color: "334155",
paraSpaceAfter: 8, // item spacing (never lineSpacing)
margin: 0, // align glyph to x
});
indent matters most — cut the default ~27pt to 10–16pt to kill the "floating dot" (indent = glyph→text gap in pt; try 10–16).2022(•), 25AA(▪), 2013(–), 25B8(▸) read more designed than a fat dot; mute the color (e.g. 94A3B8) to keep it subtle.slide.addShape(pres.shapes.RECTANGLE, {
x: 0.5, y: 0.8, w: 1.5, h: 3.0,
fill: { color: "FF0000" }, line: { color: "000000", width: 2 }
});
slide.addShape(pres.shapes.OVAL, { x: 4, y: 1, w: 2, h: 2, fill: { color: "0000FF" } });
slide.addShape(pres.shapes.LINE, {
x: 1, y: 3, w: 5, h: 0, line: { color: "FF0000", width: 3, dashType: "dash" }
});
// Transparency
slide.addShape(pres.shapes.RECTANGLE, {
x: 1, y: 1, w: 3, h: 2, fill: { color: "0088CC", transparency: 50 }
});
// Rounded rectangle (rectRadius works only on ROUNDED_RECTANGLE, not RECTANGLE)
slide.addShape(pres.shapes.ROUNDED_RECTANGLE, {
x: 1, y: 1, w: 3, h: 2, fill: { color: "FFFFFF" }, rectRadius: 0.1
});
// Shadow (to make a card stand out — use this, not an edge stripe)
slide.addShape(pres.shapes.RECTANGLE, {
x: 1, y: 1, w: 3, h: 2, fill: { color: "FFFFFF" },
shadow: { type: "outer", color: "000000", blur: 6, offset: 2, angle: 45, opacity: 0.15 }
});
Shadow options:
| Property | Range / notes |
|---|---|
type | "outer" / "inner" |
color | 6-char hex (no#, no 8-char hex) |
blur | 0–100 pt |
offset | 0–200 pt,must be non-negative (negatives corrupt the file) |
angle | 0–359°, clockwise from 3 o'clock (45 = bottom-right, 135 = bottom-left, 270 = upward) |
opacity | 0.0–1.0 (use this for transparency, never encode it incolor) |
To cast a shadow upward (e.g., a card near the bottom), use
angle: 270+ a positive offset — not a negative offset.
Gradient fills are not natively supported — use a gradient image as the background instead.
// Three sources
slide.addImage({ path: "images/photo.jpg", x: 1, y: 1, w: 5, h: 3 }); // local
slide.addImage({ path: "https://example.com/img.jpg", x: 1, y: 1, w: 5, h: 3 }); // URL
slide.addImage({ data: "image/png;base64,iVBORw0KGgo...", x: 1, y: 1, w: 5, h: 3 }); // base64 (faster)
// Options
slide.addImage({
path: "image.png", x: 1, y: 1, w: 5, h: 3,
rotate: 45, rounding: true /*circular crop*/, transparency: 50,
flipH: true, flipV: false, altText: "Description",
hyperlink: { url: "https://example.com" }
});
// Sizing modes
{ sizing: { type: 'contain', w: 4, h: 3 } } // fit inside, preserve ratio
{ sizing: { type: 'cover', w: 4, h: 3 } } // fill area, preserve ratio (may crop)
{ sizing: { type: 'crop', x: 0.5, y: 0.5, w: 2, h: 2 } } // cut a specific portion
// Compute size from aspect ratio and center it (W = the canvas-width constant, never a literal)
const origW = 1978, origH = 923, maxH = 3.0;
const calcW = maxH * (origW / origH);
const centerX = (W - calcW) / 2;
slide.addImage({ path: "image.png", x: centerX, y: 1.2, w: calcW, h: maxH });
Supports PNG / JPG / GIF / SVG (SVG works in modern PowerPoint / Microsoft 365).
outEnd labels on stacked bar chartsOOXML restricts c:dLblPos by grouping: stacked / percentStacked only allow
ctr / inBase / inEnd — outEnd is valid only for clustered (line: ctr/l/r/t/b;
pie/doughnut: bestFit/ctr/inEnd/outEnd).
pptxgenjs won't stop you from writing barGrouping: "stacked" +
dataLabelPosition: "outEnd", and LibreOffice renders it fine — only PowerPoint rejects it,
triggers "repair", and drops the chart.
To highlight a single bar, use per-point colors instead:
slide.addChart(p.charts.BAR, [{
name: "series",
labels: ["A", "B", "C"],
values: [10, 20, 30],
}], {
barDir: "col",
varyColors: true, // per-point coloring
chartColors: ["17457E", "17457E", "E8590C"], // highlight the 3rd bar
showValue: true,
dataLabelPosition: "outEnd", // valid under clustered grouping
});
slide.background = { color: "F1F1F1" }; // solid
slide.background = { color: "FF3399", transparency: 50 }; // with transparency
slide.background = { path: "https://example.com/bg.jpg" }; // image URL
slide.background = { data: "image/png;base64,iVBORw0KGgo..." };// image base64
slide.addTable([
["Header 1", "Header 2"],
["Cell 1", "Cell 2"]
], { x: 1, y: 1, w: 8, h: 2, border: { pt: 1, color: "999999" }, fill: { color: "F1F1F1" } });
// Merged cells
let tableData = [
[{ text: "Header", options: { fill: { color: "6699CC" }, color: "FFFFFF", bold: true } }, "Cell"],
[{ text: "Merged", options: { colspan: 2 } }]
];
slide.addTable(tableData, { x: 1, y: 3.5, w: 8, colW: [4, 4] });
Principle: keep charts native and editable. Choose your approach by what PowerPoint can represent, not by what's quickest to code:
addChart(); never render to an image.addImage().// Bar
slide.addChart(pres.charts.BAR, [{
name: "Sales", labels: ["Q1","Q2","Q3","Q4"], values: [4500,5500,6200,7100]
}], { x: 0.5, y: 0.6, w: 6, h: 3, barDir: 'col', showTitle: true, title: 'Quarterly Sales' });
// Line
slide.addChart(pres.charts.LINE, [{
name: "Temp", labels: ["Jan","Feb","Mar"], values: [32,35,42]
}], { x: 0.5, y: 2.5, w: 6, h: 2.5, lineSize: 3, lineSmooth: true });
// Pie
slide.addChart(pres.charts.PIE, [{
name: "Share", labels: ["A","B","Other"], values: [35,45,20]
}], { x: 6.5, y: 1, w: 3, h: 3, showPercent: true });
Make charts look modern (defaults look dated):
slide.addChart(pres.charts.BAR, chartData, {
x: 0.5, y: 1, w: 9, h: 4, barDir: "col",
chartColors: ["0D9488", "14B8A6", "5EEAD4"], // match your palette
chartArea: { fill: { color: "FFFFFF" }, roundedCorners: true },
catAxisLabelColor: "64748B", valAxisLabelColor: "64748B", // muted axis labels
valGridLine: { color: "E2E8F0", size: 0.5 }, // subtle grid, value axis only
catGridLine: { style: "none" },
showValue: true, dataLabelPosition: "outEnd", dataLabelColor: "1E293B", // data labels
showLegend: false, // hide legend for single series
});
// Master
pres.defineSlideMaster({
title: 'TITLE_SLIDE', background: { color: '283A5E' },
objects: [{ placeholder: { options: { name: 'title', type: 'title', x: 1, y: 2, w: 8, h: 2 } } }]
});
let titleSlide = pres.addSlide({ masterName: "TITLE_SLIDE" });
titleSlide.addText("My Title", { placeholder: "title" });
// Speaker notes (visible only in Presenter View, not on the slide)
slide.addNotes("Open with the FY25 revenue headline; pause after the number. If asked about the Q3 dip: supply chain, resolved in Q4.");
# with hex — corrupts the file: color: "FF0000" ✅ / "#FF0000" ❌"00000020") corrupts the file; use the opacity propertybullet: true — never unicode • (double bullets)breakLine: true between array itemslineSpacing with bullets (excessive gaps) — use paraSpaceAfter insteadpptxgen() objectconst makeShadow = () => ({ type:"outer", blur:6, offset:2, color:"000000", opacity:0.15 });
slide.addShape(pres.shapes.RECTANGLE, { shadow: makeShadow(), ... }); // ✅
fill tint or shadow to set them apart<a:pPr> per run — two or more runs in a single paragraph produce duplicate, schema-invalid paragraph properties that only PowerPoint chokes on; see the note under "Text & formatting"{type, data, options})"left" / "center" / "right""outEnd" / "inEnd" / "center"Route here when the user supplies a .pptx and wants a NEW deck built on it. Working from a template, you can infer the deck's design system — its layouts, typography, spacing, colors, and recurring content patterns, including the rules embedded in the Slide Master — and apply those conventions consistently to new material. Two non-negotiables: study the template BEFORE writing any content, and verify AFTER building — most template failures come from skipping one of the two.
Template inheritance (mandatory) — If the user provides an existing PPT, a corporate template, or a reference file, you must follow the "template inheritance" flow rather than recreating a look-alike from scratch:
- Analyze fonts, color scheme, spacing, footers, page numbers, placeholders, and brand elements.
- Build a mapping between source pages and new pages.
- Inherit existing layouts as much as possible instead of recreating a new set.
- Only modify elements that are allowed to be modified.
- Preserve the original template's visual language unless the user asks for a redesign.
- If no page in the original template can carry the target content, state the limitation explicitly and propose the closest alternative.
1. Decide the mode — "use as template" hides two different jobs:
| Mode | User signal | Build |
|---|---|---|
| Clone & fill | new outline or free structure — "做成这个风格" / "用这个模板做一份 X" | clone template pages and fill them; pick which pages to reuse by role and shape your outline around what the template offers |
| Fill-in | new content maps ≈1:1 onto the template's pages (换数据 / 换客户) | in-place replacement — Approach A of the Editing section |
Both modes edit the .pptx natively and output an editable PowerPoint — never route a template job through the from-scratch HTML pipeline; a foreign HTML-rendered page next to the template's real pages is instantly visible and loses the theme. Clone & fill builds a new deck on the template; Fill-in is the lighter in-place path. Everything below applies to both.
2. Study the template (before generating any content) — programmatic first, vision only to break ties:
• Inventory every text shape with python-pptx (recurse into GROUPs): position, size, font, current text length. The original text length is the budget ceiling — the "Budget every shape" rules of the Editing section apply verbatim. Collect this first; it also drives the role classification below.
• Classify each page's role (cover / section / content / stats / quote / closing…) — this role map is your layout catalog for planning. Derive the role from the inventory, not from an image: cover = few shapes + an outsized title (≥36pt) and/or a full-page background image; section = only 1–2 short text shapes; stats = a 60pt+ numeric shape; quote = a single large centered long-text box; content = several body boxes. Derive every page's role from the inventory alone — never fall back to rendering an image.
• Answer the one decisive structural question: does the design live in slideLayouts with real placeholders (→ clone & fill can add_slide + fill), or is it drawn on slides with free text boxes while layouts sit empty (common in downloaded templates; → clone & fill must clone slides at XML level)?
• Read the design's source of truth: presentation.xml for slide size (never assume 16:9); theme1.xml for colors and fonts — slide XML uses schemeClr indirection, the real hex lives in the theme (remapped by the master's clrMap), and CJK text renders in the <a:ea> font.
3. Plan, then build — always on a COPY of the user's file:
• Decide the output path BEFORE you modify anything. By default save to a NEW sibling file
(<stem>_updated.pptx), never touch the input; overwrite in place ONLY if the user explicitly
asks to edit their own file — then first copy it to <stem>_backup.pptx next to it (never
/tmp). Presentation.save(path) clobbers whatever path you name, so never pass it the user's
original.
• Write a short plan first: final page order; each page's template source (slide index to clone, or layout name) chosen by ROLE; replacement text written WITHIN the budget. If the user's outline and the template's structure conflict, surface the trade-off instead of improvising. (Clone & fill: since structure is free, let the template's available page roles drive the outline rather than forcing a shape the template can't carry.)
• Fill-in: follow the Editing section's Approach A as written.
• Clone & fill: materialize pages first — add_slide + fill placeholders when layouts are real; otherwise clone the slide at XML level (copy the slide part + its .rels, re-register media rIds, add to sldIdLst and [Content_Types].xml). No helper scripts ship with this skill — write the clone yourself, either in-process (copy.deepcopy(src.slides[i]._element) + prs.part.relate_to(...) to rebuild each rel) or by unpacking the file with zipfile (ZipFile.extractall → edit the XML → repack: write [Content_Types].xml as the first entry, use ZIP_DEFLATED, and add no directory entries). Then replace content page by page (scope mappings per slide — clones share identical source text), swap images by replacing the image part bytes (keep the shape and rId), and delete unused template pages LAST, high-index first. Never hand-build a from-scratch page next to template pages — a foreign page is instantly visible.
4. Verify (mandatory before reporting done) — programmatic checks, no rendering:
The whole checklist is decidable without an image; run these and fix until clean:
• Page order & count — count sldIdLst against the plan.
• Leftover placeholders — grep the slide XML for Click to add, xxx, lorem, TODO, [insert.
• Broken images — confirm every image rId on a cloned page resolves to a real media part (empty frames come from dangling rels): for each slide walk slide.part.rels and assert every non-external rel has a target_part with a non-empty .blob.
• File still opens — reopen the saved copy with Presentation(path) and walk every slide's shapes. A dangling rel or malformed slide XML raises here; this load-and-walk is the XML validity check (there is no validate.py in this skill).
• Fonts & colors unchanged — diff the run/theme font+color against the original rather than eyeballing it.
• Overflow / collision — re-check the "Budget every shape" rule (len ≤ orig_len × 1.1) on the final text and compare each left+width against its neighbor. A passing budget is the overflow guard here — treat any shape over budget as a real defect and fix it (trim, widen, or shrink per the Editing rules).
For in-place edits to an existing deck (Fill-in mode, and small fixes like typos or updating numbers), work on a COPY and pick the approach by what the edit touches:
Output first, modify second. By default save edits to a NEW sibling file (
<stem>_updated.pptx),
never touch the input; overwrite in place ONLY if the user explicitly asks to edit their own file —
then first copy it to<stem>_backup.pptxnext to it (never/tmp). "Work on a copy" means the
original stays byte-identical at its original path, not merely that you opened it read-only.
python-pptx script — preferred for text replacement, deleting/reordering slides, and any edit that should preserve fonts/colors/layout. Simpler and safer than raw XML for content swaps.python-pptx can't reach.python-pptx text replacement (preferred for text edits)Workflow
Inventory the deck — walk every slide, recurse into GROUP shapes (shape_type == 6), print(repr(para.text)). Use the inventory as the source of truth for replacement keys; rendered text often contains hidden chars that won't survive copy-paste.
Helpers — keep the build script short:
from pptx import Presentation
from pptx.enum.text import MSO_AUTO_SIZE
from pptx.oxml.ns import qn
from pptx.util import Emu, Pt
def iter_text_frames(shapes):
for s in shapes:
if s.shape_type == 6: # GROUP → recurse
yield from iter_text_frames(s.shapes)
elif s.has_text_frame:
yield s, s.text_frame
def _norm(s): # strip soft breaks before matching
return s.replace("\x0b", "").replace("\r", "").strip()
def replace_in_paragraph(p, new_text): # first-run replace preserves formatting
runs = p.runs
if not runs:
p.add_run().text = new_text; return
runs[0].text = new_text
for r in runs[1:]:
r._r.getparent().remove(r._r)
def apply_replacements(tf, mapping): # full-frame match, then per-paragraph
m = {_norm(k): v for k, v in mapping.items()}
full = "\n".join(p.text for p in tf.paragraphs)
if _norm(full) in m:
parts = m[_norm(full)].split("\n")
for i, p in enumerate(tf.paragraphs):
replace_in_paragraph(p, parts[i] if i < len(parts) else "")
return
for p in tf.paragraphs:
if _norm(p.text) in m:
replace_in_paragraph(p, m[_norm(p.text)])
def delete_slide(prs, idx): # call high-index first
sld = list(prs.slides._sldIdLst)[idx]
prs.part.drop_rel(sld.get(qn("r:id")))
prs.slides._sldIdLst.remove(sld)
Budget every shape BEFORE generating replacement text (do this first)
Most overflow bugs come from generating copy without knowing the target box's capacity. Before drafting any replacement, walk the deck once and emit a capacity manifest — then feed it to the content step as a hard constraint.
For each text-bearing shape collect: slide_idx, shape_id, w_cm, h_cm, font_pt, orig_text, orig_len. Then:
budget = orig_len × 1.1. The template designer already tuned orig_len for this box — treat it as the ceiling, not a starting point. This one rule is the actual overflow guard; don't over-engineer it with width/line-height estimates (glyph advance widths vary by font and by CJK-vs-Latin mix, so any chars_per_line formula is a rough guess that the orig_len cap already subsumes).role = "label" if h_cm < 1.5 OR orig_len ≤ 8 OR font_pt ≥ 20; else "body".Rules the generation step MUST obey:
max(orig_len, 8). SWOT tiles, timeline tags, KPI labels all fall here.budget. Font size is inherited from the template; shrinking is a last resort, not plan A.widen_to_fit(shape, Emu(...)) — see below) rather than shrinking the font. Check first that left + width won't collide with the next shape.Handling long replacement / unwanted wrapping after replacement
When a longer replacement wraps to a new line, apply remedies in this order (cheapest first):
def widen_to_fit(shape, max_grow_emu=Emu(0)):
"""Let PowerPoint size the shape to its text. Pass max_grow_emu>0 to also
grow the explicit width (centered on the original position) before sizing."""
if max_grow_emu:
shape.left -= max_grow_emu // 2
shape.width += max_grow_emu
shape.text_frame.word_wrap = True
shape.text_frame.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
def shrink_text_to_fit(shape):
"""Keep the box fixed; let PowerPoint shrink the font to fit."""
shape.text_frame.word_wrap = True
shape.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
⚠️ Both helpers only write the autofit flag into the XML — python-pptx does not compute the resized shape or the shrunk font-scale itself. The actual fit is applied by the viewer (PowerPoint / LibreOffice) when the file is opened, so your programmatic overflow check can't see the result. Prefer trimming to
budget(below), which is verifiable without rendering.
shape.width × font_size from inventory and trim the replacement so it fits the original visual budget. Numeric badges / small label boxes (width ≤ 0.7", font_size ≥ 16pt) hold ~3–4 chars max.widen_to_fit(shape, Emu(...)) when the content is genuinely longer and there's free space next to it. Always check the shape isn't going to collide with a neighbor first (compare left+width against the next shape's left).shrink_text_to_fit(shape) only for tight-layout boxes (table cells, numeric badges) where widening would break the grid. Last resort — it visibly breaks the typographic rhythm.Skip word_wrap = False: it makes text overflow the box invisibly in PowerPoint and looks broken when exported.
Critical gotchas
\x0b) silently break exact-match. Always _norm() both keys and lookups.shape_type == 6) hide text frames — recurse.paragraph.text = ... destroys it."01", "%", "18" recur across slides — keep identity mappings or scope per slide index, never global cross-mappings like "18": "12".IMPORTANT: When generating code for PPTX operations:
Required dependencies (should already be installed):
pip install "markitdown[pptx]" (text extraction)npm install -g pptxgenjs (creating presentations)npm install -g playwright@1.50.0 (HTML rendering)npm install -g sharp (SVG rasterization and image processing)https://mirrors.tuna.tsinghua.edu.cn/libreoffice/libreoffice/stable/26.8.0/, see the hard
requirement below); sudo apt-get install libreoffice only as a fallbacksudo apt-get install poppler-utils (pdftoppm)pip install defusedxml (secure XML parsing)Every PPTX → PDF conversion and every rendered preview/thumbnail step depends on LibreOffice.
If LibreOffice is not installed, you MUST install it. This is not a choice.
FORBIDDEN — do not do any of these:
REQUIRED sequence — no steps skipped, in this order:
Explain the necessity to the user. LibreOffice is what renders the deck's real layout — master slides, themes, placeholder geometry — so it is the only way to verify slides actually fit before delivery. Substituting it means shipping an unverified deck.
Tell them the download is large (roughly several hundred MB) and may take a while. This is information for the user, not a decision point for you — state it and proceed to install.
Install it now. Preferred source — Tsinghua TUNA mirror (fast in China, current build):
https://mirrors.tuna.tsinghua.edu.cn/libreoffice/libreoffice/stable/26.8.0/. Pick the
subdirectory matching the OS and CPU arch (uname -m / $env:PROCESSOR_ARCHITECTURE) and
download the main package from it:
| Platform | Path | Main package |
|---|---|---|
| Linux x86_64 (deb) | deb/x86_64/ | LibreOffice_26.8.0_Linux_x86-64_deb.tar.gz |
| Linux ARM64 (deb) | deb/aarch64/ | LibreOffice_26.8.0_Linux_aarch64_deb.tar.gz |
| Linux x86_64 (rpm) | rpm/x86_64/ | LibreOffice_26.8.0_Linux_x86-64_rpm.tar.gz |
| macOS Apple Silicon | mac/aarch64/ | LibreOffice_26.8.0_MacOS_aarch64.dmg |
| macOS Intel | mac/x86_64/ | LibreOffice_26.8.0_MacOS_x86-64.dmg |
| Windows x64 | win/x86_64/ | LibreOffice_26.8.0_Win_x86-64.msi |
| Windows ARM64 | win/aarch64/ | LibreOffice_26.8.0_Win_aarch64.msi |
Linux (deb): tar -xzf <pkg>.tar.gz && sudo dpkg -i LibreOffice_26.8.0*/DEBS/*.deb && sudo apt-get install -f -y
macOS: hdiutil attach <pkg>.dmg && cp -R /Volumes/LibreOffice/LibreOffice.app /Applications/ && hdiutil detach /Volumes/LibreOffice
Windows: msiexec /i <pkg>.msi /qn (needs admin). Verify with soffice --version.
If 26.8.0 is gone (the mirror keeps only a few releases), list
https://mirrors.tuna.tsinghua.edu.cn/libreoffice/libreoffice/stable/ and use the newest version
directory, substituting that version number above.
Package-manager fallback, only if the mirror is unreachable:
brew install --cask libreofficesudo apt-get install libreofficewinget install TheDocumentFoundation.LibreOfficeRegister the binary on PATH, then re-verify. Do this as part of the install, not as an
afterthought: an install that leaves soffice off the executable search path is
indistinguishable from "not installed" to the next command -v soffice probe — which re-triggers
this whole block and makes you download several hundred MB you already have. The mechanism
differs per OS — use the one for the platform you are on:
| Platform | Register it | Then verify |
|---|---|---|
| Linux | sudo ln -sf /opt/libreoffice*/program/soffice /usr/local/bin/soffice | soffice --version |
| macOS | sudo ln -sf /Applications/LibreOffice.app/Contents/MacOS/soffice /usr/local/bin/soffice | soffice --version |
| Windows | setx PATH "$([Environment]::GetEnvironmentVariable('PATH','User'));C:\Program Files\LibreOffice\program" | & "C:\Program Files\LibreOffice\program\soffice.exe" --version |
Notes: on Apple Silicon /usr/local/bin is not always on PATH — if the verify still fails, link
into /opt/homebrew/bin instead. On Windows setx only affects newly launched shells, so for
the rest of the current session call soffice.exe by its full path. Only continue once the verify
prints a version. Corollary: probe before installing — if the binary already exists on disk
but off-PATH (check /opt/libreoffice*/program/soffice,
/Applications/LibreOffice.app/Contents/MacOS/soffice,
C:\Program Files\LibreOffice\program\soffice.exe), register it instead of reinstalling.
Only after that install has actually been attempted AND failed may you use software already on the user's machine (Keynote, PowerPoint, WPS). Report the install failure and what you switched to, and warn that the rendering may differ from this skill's normal checks.
The gate is simple: no install attempt = no substitute program. If you have not run the
install command and seen it fail, using Keynote/PowerPoint/WPS is a violation of this skill.
Place ::zcode-file-citation{...} inline in prose, not in a trailing list. Use purpose="source" for Q&A/no-op and purpose="output" for create/edit.
Created ::zcode-file-citation{path="/abs/path/launch-deck.pptx" purpose="output"}, highlighting the rollout and owners.inspect the complete relevant slide, including callouts, question wording, chart/table titles, totals/sample sizes, and source/methodology footers. Answer directly, group same-slide claims, and cite that slide once. For concrete chart/table/image/diagram/callout evidence, include exact inspected slide_id, object_id, and a useful label when available.
For non-in-place edits, preserve the source and export a copy; if unchanged, cite the source plainly.
Use only locators verified against the latest render/inspection:
::zcode-file-citation{path="/abs/path/deck.pptx" purpose="source" artifact_kind="presentation" slide_number=3}
::zcode-file-citation{path="/abs/path/deck.pptx" purpose="source" artifact_kind="presentation" slide_number=1 slide_id="sl/gs5z1kshq0xv" object_id="ch/pz9t1r3ka8vn" label="ARR by segment chart"}
If IDs are not exact, stop at slide_number; never guess or cite intermediates unless asked.
아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
three.js로 입체 모델을 만들고 회전 가능한 뷰어에서 모든 각도로 확인합니다. OBJ + MTL이나 GLB로 저장해 다른 3D 제작 소프트웨어에서 사용할 수 있습니다.
Audit web accessibility with Chrome DevTools MCP and Lighthouse. Inspect semantic structure, labels, keyboard focus, tap target size, and color contrast, then identify page elements that may need changes based on web.dev guidance.
한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.
개인 작업 목록에 할 일, 기한, 다음 단계, 답변을 기다리는 대상과 진행을 막는 이유를 기록합니다. 진행 상황과 의존 관계를 갱신하고 중복 및 완료 항목을 정리합니다.
Coordinate a task from a verifiable goal through completion. Handle work directly or split independent tasks across separate agent sessions, track progress, verify results, and arrange pull request merging when included in the goal.
한국어 요약은 준비 중입니다. 영어 요약을 표시하고 있습니다.
Antigravity가 사용자 지정 항목을 발견하고 불러오는 위치와 우선순위를 알아봅니다. 에이전트 작업 흐름과 외부 도구 연결에 맞춰 스킬, 규칙, 플러그인, 훅, MCP 서버를 선택하고 만드는 방법을 안내합니다.
재생, 일시 정지, 시간 위치 지정이 가능한 HTML 애니메이션을 만듭니다. 장면 길이와 속도를 조절하고 가로형·세로형 영상 내보내기를 준비합니다.