update: add minimax-pdf skill and reorganize pptx-generator references
- Add minimax-pdf skill (CREATE / FILL / REFORMAT workflows) - Move pptx-generator reference docs into references/ subdirectory - Update README.md and README_zh.md with new skill entries
This commit is contained in:
@@ -16,6 +16,8 @@ Development skills for AI coding agents. Plug into your favorite AI coding tool
|
|||||||
| `ios-application-dev` | iOS application development guide covering UIKit, SnapKit, and SwiftUI. Touch targets, safe areas, navigation patterns, Dynamic Type, Dark Mode, accessibility, collection views, and Apple HIG compliance. |
|
| `ios-application-dev` | iOS application development guide covering UIKit, SnapKit, and SwiftUI. Touch targets, safe areas, navigation patterns, Dynamic Type, Dark Mode, accessibility, collection views, and Apple HIG compliance. |
|
||||||
| `shader-dev` | Comprehensive GLSL shader techniques for creating stunning visual effects — ray marching, SDF modeling, fluid simulation, particle systems, procedural generation, lighting, post-processing, and more. ShaderToy-compatible. |
|
| `shader-dev` | Comprehensive GLSL shader techniques for creating stunning visual effects — ray marching, SDF modeling, fluid simulation, particle systems, procedural generation, lighting, post-processing, and more. ShaderToy-compatible. |
|
||||||
| `gif-sticker-maker` | Convert photos (people, pets, objects, logos) into 4 animated GIF stickers with captions. Funko Pop / Pop Mart style, powered by MiniMax Image & Video Generation API. |
|
| `gif-sticker-maker` | Convert photos (people, pets, objects, logos) into 4 animated GIF stickers with captions. Funko Pop / Pop Mart style, powered by MiniMax Image & Video Generation API. |
|
||||||
|
| `minimax-pdf` | Generate, fill, and reformat PDF documents with a token-based design system. CREATE polished PDFs from scratch (15 cover styles), FILL existing form fields, or REFORMAT documents into a new design. Print-ready output with typography and color derived from document type. |
|
||||||
|
| `pptx-generator` | Generate, edit, and read PowerPoint presentations. Create from scratch with PptxGenJS (cover, TOC, content, section divider, summary slides), edit existing PPTX via XML workflows, or extract text with markitdown. |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
| `ios-application-dev` | iOS 应用开发指南,涵盖 UIKit、SnapKit 和 SwiftUI。触控目标、安全区域、导航模式、Dynamic Type、深色模式、无障碍、集合视图,符合 Apple HIG 规范。 |
|
| `ios-application-dev` | iOS 应用开发指南,涵盖 UIKit、SnapKit 和 SwiftUI。触控目标、安全区域、导航模式、Dynamic Type、深色模式、无障碍、集合视图,符合 Apple HIG 规范。 |
|
||||||
| `shader-dev` | 全面的 GLSL 着色器技术,用于创建惊艳的视觉效果 — 光线行进、SDF 建模、流体模拟、粒子系统、程序化生成、光照、后处理等。兼容 ShaderToy。 |
|
| `shader-dev` | 全面的 GLSL 着色器技术,用于创建惊艳的视觉效果 — 光线行进、SDF 建模、流体模拟、粒子系统、程序化生成、光照、后处理等。兼容 ShaderToy。 |
|
||||||
| `gif-sticker-maker` | 将照片(人物、宠物、物品、Logo)转换为 4 张带字幕的动画 GIF 贴纸。Funko Pop / Pop Mart 盲盒风格,基于 MiniMax 图片与视频生成 API。 |
|
| `gif-sticker-maker` | 将照片(人物、宠物、物品、Logo)转换为 4 张带字幕的动画 GIF 贴纸。Funko Pop / Pop Mart 盲盒风格,基于 MiniMax 图片与视频生成 API。 |
|
||||||
|
| `minimax-pdf` | 基于 token 化设计系统生成、填写和重排 PDF 文档。支持三种模式:CREATE(从零生成,15 种封面风格)、FILL(填写现有表单字段)、REFORMAT(将已有文档重排为新设计)。排版与配色由文档类型自动推导,输出即可打印。 |
|
||||||
|
| `pptx-generator` | 生成、编辑和读取 PowerPoint 演示文稿。支持用 PptxGenJS 从零创建(封面、目录、内容、分节页、总结页),通过 XML 工作流编辑现有 PPTX,或用 markitdown 提取文本。 |
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
|
|||||||
222
skills/minimax-pdf/README.md
Normal file
222
skills/minimax-pdf/README.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# minimax-pdf
|
||||||
|
|
||||||
|
A Claude skill for creating and editing visually polished PDFs.
|
||||||
|
Three routes. One design system. Tokens flow from content analysis through every renderer.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/make.sh check # verify deps
|
||||||
|
bash scripts/make.sh fix # auto-install missing deps
|
||||||
|
bash scripts/make.sh demo # → demo.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route A: CREATE — generate a new PDF
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/make.sh run \
|
||||||
|
--title "Q3 Strategy Review" \
|
||||||
|
--type "proposal" \
|
||||||
|
--author "Strategy Team" \
|
||||||
|
--date "October 2025" \
|
||||||
|
--content content.json \
|
||||||
|
--out report.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
**`--type` options:**
|
||||||
|
|
||||||
|
| Type | Palette | Cover pattern | Google Fonts (cover) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `report` | Deep ink, teal accent | `fullbleed` | Playfair Display / IBM Plex Sans |
|
||||||
|
| `proposal` | Near-black, amber accent | `split` | Syne / Nunito Sans |
|
||||||
|
| `resume` | White, navy accent | `typographic` | DM Serif Display / DM Sans |
|
||||||
|
| `portfolio` | Deep violet, coral accent | `atmospheric` | Fraunces / Inter |
|
||||||
|
| `academic` | Warm white, navy accent | `typographic` | EB Garamond / Source Sans 3 |
|
||||||
|
| `general` | Dark slate, blue accent | `fullbleed` | Outfit / Outfit |
|
||||||
|
| `minimal` | Near-white, red accent | `minimal` | Cormorant Garamond / Jost |
|
||||||
|
| `stripe` | Dark navy, amber accent | `stripe` | Barlow Condensed / Barlow |
|
||||||
|
| `diagonal` | Dark blue, teal accent | `diagonal` | Montserrat / Montserrat |
|
||||||
|
| `frame` | Warm cream, brown accent | `frame` | Cormorant / Crimson Pro |
|
||||||
|
| `editorial` | White, red accent | `editorial` | Bebas Neue / Libre Franklin |
|
||||||
|
| `magazine` | Warm linen, deep navy accent | `magazine` | Playfair Display / EB Garamond |
|
||||||
|
| `darkroom` | Deep navy, steel blue accent | `darkroom` | Playfair Display / EB Garamond |
|
||||||
|
| `terminal` | Near-black, neon green accent | `terminal` | Space Mono |
|
||||||
|
| `poster` | White, near-black accent | `poster` | Barlow Condensed / Courier Prime |
|
||||||
|
|
||||||
|
**content.json block types:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "h1", "text": "Section Title"},
|
||||||
|
{"type": "h2", "text": "Subsection"},
|
||||||
|
{"type": "h3", "text": "Sub-subsection"},
|
||||||
|
{"type": "body", "text": "Paragraph. Supports <b>bold</b> and <i>italic</i>."},
|
||||||
|
{"type": "bullet", "text": "Unordered list item"},
|
||||||
|
{"type": "numbered","text": "Ordered list item — counter auto-resets between lists"},
|
||||||
|
{"type": "callout", "text": "Key insight or highlighted finding"},
|
||||||
|
{"type": "table",
|
||||||
|
"headers": ["Col A", "Col B"],
|
||||||
|
"rows": [["a", "b"], ["c", "d"]]
|
||||||
|
},
|
||||||
|
{"type": "image", "path": "chart.png", "caption": "Figure 1: optional caption"},
|
||||||
|
{"type": "code", "text": "def hello():\n print('world')"},
|
||||||
|
{"type": "math", "text": "\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}", "label": "(1)"},
|
||||||
|
{"type": "divider"},
|
||||||
|
{"type": "caption", "text": "Table 1: standalone caption label"},
|
||||||
|
{"type": "pagebreak"},
|
||||||
|
{"type": "spacer", "pt": 16}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route B: FILL — fill form fields in an existing PDF
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# See what fields the PDF has
|
||||||
|
bash scripts/make.sh fill --input form.pdf --inspect
|
||||||
|
|
||||||
|
# Fill fields
|
||||||
|
bash scripts/make.sh fill \
|
||||||
|
--input form.pdf \
|
||||||
|
--out filled.pdf \
|
||||||
|
--values '{"FirstName": "Jane", "Agree": "true", "Country": "US"}'
|
||||||
|
|
||||||
|
# Or from a JSON file
|
||||||
|
bash scripts/make.sh fill --input form.pdf --out filled.pdf --data values.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Field value rules:
|
||||||
|
- `text` → any string
|
||||||
|
- `checkbox` → `"true"` or `"false"`
|
||||||
|
- `dropdown` → must match a choice value shown by `--inspect`
|
||||||
|
- `radio` → must match a radio value shown by `--inspect`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route C: REFORMAT — apply design to an existing document
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/make.sh reformat \
|
||||||
|
--input source.md \
|
||||||
|
--title "Annual Report" \
|
||||||
|
--type "report" \
|
||||||
|
--author "Research Team" \
|
||||||
|
--out output.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
Supported input: `.md` `.txt` `.pdf` `.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
SKILL.md ← Claude entry point, route table
|
||||||
|
design/design.md ← Aesthetic system (read before CREATE/REFORMAT)
|
||||||
|
scripts/
|
||||||
|
make.sh ← Unified CLI
|
||||||
|
palette.py ← metadata → tokens.json [CREATE, REFORMAT]
|
||||||
|
cover.py ← tokens.json → cover.html [CREATE, REFORMAT]
|
||||||
|
render_cover.js ← cover.html → cover.pdf [CREATE, REFORMAT]
|
||||||
|
render_body.py ← tokens + content → body.pdf [CREATE, REFORMAT]
|
||||||
|
merge.py ← cover + body → final.pdf [CREATE, REFORMAT]
|
||||||
|
fill_inspect.py ← PDF → field list [FILL]
|
||||||
|
fill_write.py ← PDF + values → filled PDF [FILL]
|
||||||
|
reformat_parse.py ← doc → content.json [REFORMAT]
|
||||||
|
```
|
||||||
|
|
||||||
|
Design tokens (`tokens.json`) flow from `palette.py` to every renderer — cover and body are always visually consistent.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Tool | Used by | Install |
|
||||||
|
|---|---|---|
|
||||||
|
| Python 3.9+ | all `.py` scripts | system |
|
||||||
|
| `reportlab` | `render_body.py` | `pip install reportlab` |
|
||||||
|
| `pypdf` | fill, merge, reformat | `pip install pypdf` |
|
||||||
|
| Node.js 18+ | `render_cover.js` | system |
|
||||||
|
| `playwright` + Chromium | `render_cover.js` | `npm install -g playwright && npx playwright install chromium` |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Document types
|
||||||
|
|
||||||
|
| `--type` | Mood | Cover pattern | Cover fonts |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `report` | Authoritative | `fullbleed` | Playfair Display / IBM Plex Sans |
|
||||||
|
| `proposal` | Confident | `split` | Syne / Nunito Sans |
|
||||||
|
| `resume` | Clean | `typographic` | DM Serif Display / DM Sans |
|
||||||
|
| `portfolio` | Expressive | `atmospheric` | Fraunces / Inter |
|
||||||
|
| `academic` | Scholarly | `typographic` | EB Garamond / Source Sans 3 |
|
||||||
|
| `general` | Neutral | `fullbleed` | Outfit |
|
||||||
|
| `minimal` | Restrained | `minimal` | Cormorant Garamond / Jost |
|
||||||
|
| `stripe` | Bold | `stripe` | Barlow Condensed / Barlow |
|
||||||
|
| `diagonal` | Dynamic | `diagonal` | Montserrat |
|
||||||
|
| `frame` | Classical | `frame` | Cormorant / Crimson Pro |
|
||||||
|
| `editorial` | Editorial | `editorial` | Bebas Neue / Libre Franklin |
|
||||||
|
|
||||||
|
Cover fonts load via Google Fonts `@import` at render time — no local caching.
|
||||||
|
Body pages always use system fonts (Times / Helvetica) via ReportLab.
|
||||||
|
|
||||||
|
## content.json schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"type": "h1", "text": "Section Title"},
|
||||||
|
{"type": "h2", "text": "Subsection"},
|
||||||
|
{"type": "h3", "text": "Sub-subsection"},
|
||||||
|
{"type": "body", "text": "Paragraph text. Supports <b>bold</b> and <i>italic</i>."},
|
||||||
|
{"type": "bullet", "text": "Unordered list item"},
|
||||||
|
{"type": "numbered","text": "Ordered list item — auto-numbered, counter resets between lists"},
|
||||||
|
{"type": "callout", "text": "Highlighted insight or key finding"},
|
||||||
|
{"type": "table",
|
||||||
|
"headers": ["Column A", "Column B", "Column C"],
|
||||||
|
"rows": [["row1a", "row1b", "row1c"], ["row2a", "row2b", "row2c"]]
|
||||||
|
},
|
||||||
|
{"type": "image", "path": "chart.png", "caption": "Figure 1: Sales by quarter"},
|
||||||
|
{"type": "code", "text": "SELECT * FROM users\nWHERE active = 1;"},
|
||||||
|
{"type": "math", "text": "\\sigma = \\sqrt{\\frac{1}{N}\\sum_{i=1}^N (x_i - \\mu)^2}", "label": "(2)"},
|
||||||
|
{"type": "divider"},
|
||||||
|
{"type": "caption", "text": "Table 2: standalone label"},
|
||||||
|
{"type": "pagebreak"},
|
||||||
|
{"type": "spacer", "pt": 16}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
SKILL.md ← Claude entry point, routing only
|
||||||
|
design/design.md ← Aesthetic system (read before any script)
|
||||||
|
scripts/
|
||||||
|
make.sh ← Unified CLI: check / fix / run / demo
|
||||||
|
palette.py ← content metadata → tokens.json
|
||||||
|
cover.py ← tokens.json → cover.html
|
||||||
|
render_cover.js ← cover.html → cover.pdf (Playwright)
|
||||||
|
render_body.py ← tokens.json + content.json → body.pdf (ReportLab)
|
||||||
|
merge.py ← cover.pdf + body.pdf → final.pdf + QA report
|
||||||
|
```
|
||||||
|
|
||||||
|
Design tokens (color, typography, spacing) are written once by `palette.py` and consumed by every downstream script. This guarantees visual consistency between cover and body without any manual coordination.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Tool | Purpose | Install |
|
||||||
|
|---|---|---|
|
||||||
|
| Python 3.9+ | palette, cover, render_body, merge | system |
|
||||||
|
| `reportlab` | Body page rendering | `pip install reportlab` |
|
||||||
|
| `pypdf` | Merging PDFs | `pip install pypdf` |
|
||||||
|
| Node.js 18+ | Cover rendering | system |
|
||||||
|
| `playwright` | Headless Chromium for cover | `npm install -g playwright && npx playwright install chromium` |
|
||||||
|
|
||||||
|
Run `bash scripts/make.sh check` to verify everything at once.
|
||||||
|
Run `bash scripts/make.sh fix` to auto-install what is missing.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
192
skills/minimax-pdf/SKILL.md
Normal file
192
skills/minimax-pdf/SKILL.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
---
|
||||||
|
name: minimax-pdf
|
||||||
|
description: >
|
||||||
|
Use this skill when visual quality and design identity matter for a PDF.
|
||||||
|
CREATE (generate from scratch): "make a PDF", "generate a report", "write a proposal",
|
||||||
|
"create a resume", "beautiful PDF", "professional document", "cover page",
|
||||||
|
"polished PDF", "client-ready document".
|
||||||
|
FILL (complete form fields): "fill in the form", "fill out this PDF",
|
||||||
|
"complete the form fields", "write values into PDF", "what fields does this PDF have".
|
||||||
|
REFORMAT (apply design to an existing doc): "reformat this document", "apply our style",
|
||||||
|
"convert this Markdown/text to PDF", "make this doc look good", "re-style this PDF".
|
||||||
|
This skill uses a token-based design system: color, typography, and spacing are derived
|
||||||
|
from the document type and flow through every page. The output is print-ready.
|
||||||
|
Prefer this skill when appearance matters, not just when any PDF output is needed.
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
version: "1.0"
|
||||||
|
category: document-generation
|
||||||
|
---
|
||||||
|
|
||||||
|
# minimax-pdf
|
||||||
|
|
||||||
|
Three tasks. One skill.
|
||||||
|
|
||||||
|
## Read `design/design.md` before any CREATE or REFORMAT work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route table
|
||||||
|
|
||||||
|
| User intent | Route | Scripts used |
|
||||||
|
|---|---|---|
|
||||||
|
| Generate a new PDF from scratch | **CREATE** | `palette.py` → `cover.py` → `render_cover.js` → `render_body.py` → `merge.py` |
|
||||||
|
| Fill / complete form fields in an existing PDF | **FILL** | `fill_inspect.py` → `fill_write.py` |
|
||||||
|
| Reformat / re-style an existing document | **REFORMAT** | `reformat_parse.py` → then full CREATE pipeline |
|
||||||
|
|
||||||
|
**Rule:** when in doubt between CREATE and REFORMAT, ask whether the user has an existing document to start from. If yes → REFORMAT. If no → CREATE.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route A: CREATE
|
||||||
|
|
||||||
|
Full pipeline — content → design tokens → cover → body → merged PDF.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/make.sh run \
|
||||||
|
--title "Q3 Strategy Review" --type proposal \
|
||||||
|
--author "Strategy Team" --date "October 2025" \
|
||||||
|
--accent "#2D5F8A" \
|
||||||
|
--content content.json --out report.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Doc types:** `report` · `proposal` · `resume` · `portfolio` · `academic` · `general` · `minimal` · `stripe` · `diagonal` · `frame` · `editorial` · `magazine` · `darkroom` · `terminal` · `poster`
|
||||||
|
|
||||||
|
| Type | Cover pattern | Visual identity |
|
||||||
|
|---|---|---|
|
||||||
|
| `report` | `fullbleed` | Dark bg, dot grid, Playfair Display |
|
||||||
|
| `proposal` | `split` | Left panel + right geometric, Syne |
|
||||||
|
| `resume` | `typographic` | Oversized first-word, DM Serif Display |
|
||||||
|
| `portfolio` | `atmospheric` | Near-black, radial glow, Fraunces |
|
||||||
|
| `academic` | `typographic` | Light bg, classical serif, EB Garamond |
|
||||||
|
| `general` | `fullbleed` | Dark slate, Outfit |
|
||||||
|
| `minimal` | `minimal` | White + single 8px accent bar, Cormorant Garamond |
|
||||||
|
| `stripe` | `stripe` | 3 bold horizontal color bands, Barlow Condensed |
|
||||||
|
| `diagonal` | `diagonal` | SVG angled cut, dark/light halves, Montserrat |
|
||||||
|
| `frame` | `frame` | Inset border, corner ornaments, Cormorant |
|
||||||
|
| `editorial` | `editorial` | Ghost letter, all-caps title, Bebas Neue |
|
||||||
|
| `magazine` | `magazine` | Warm cream bg, centered stack, hero image, Playfair Display |
|
||||||
|
| `darkroom` | `darkroom` | Navy bg, centered stack, grayscale image, Playfair Display |
|
||||||
|
| `terminal` | `terminal` | Near-black, grid lines, monospace, neon green |
|
||||||
|
| `poster` | `poster` | White bg, thick sidebar, oversized title, Barlow Condensed |
|
||||||
|
|
||||||
|
Cover extras (inject into tokens via `--abstract`, `--cover-image`):
|
||||||
|
- `--abstract "text"` — abstract text block on the cover (magazine/darkroom)
|
||||||
|
- `--cover-image "url"` — hero image URL/path (magazine, darkroom, poster)
|
||||||
|
|
||||||
|
**Color overrides — always choose these based on document content:**
|
||||||
|
- `--accent "#HEX"` — override the accent color; `accent_lt` is auto-derived by lightening toward white
|
||||||
|
- `--cover-bg "#HEX"` — override the cover background color
|
||||||
|
|
||||||
|
**Accent color selection guidance:**
|
||||||
|
|
||||||
|
You have creative authority over the accent color. Pick it from the document's semantic context — title, industry, purpose, audience — not from generic "safe" choices. The accent appears on section rules, callout bars, table headers, and the cover: it carries the document's visual identity.
|
||||||
|
|
||||||
|
| Context | Suggested accent range |
|
||||||
|
|---|---|
|
||||||
|
| Legal / compliance / finance | Deep navy `#1C3A5E`, charcoal `#2E3440`, slate `#3D4C5E` |
|
||||||
|
| Healthcare / medical | Teal-green `#2A6B5A`, cool green `#3A7D6A` |
|
||||||
|
| Technology / engineering | Steel blue `#2D5F8A`, indigo `#3D4F8A` |
|
||||||
|
| Environmental / sustainability | Forest `#2E5E3A`, olive `#4A5E2A` |
|
||||||
|
| Creative / arts / culture | Burgundy `#6B2A35`, plum `#5A2A6B`, terracotta `#8A3A2A` |
|
||||||
|
| Academic / research | Deep teal `#2A5A6B`, library blue `#2A4A6B` |
|
||||||
|
| Corporate / neutral | Slate `#3D4A5A`, graphite `#444C56` |
|
||||||
|
| Luxury / premium | Warm black `#1A1208`, deep bronze `#4A3820` |
|
||||||
|
|
||||||
|
**Rule:** choose a color that a thoughtful designer would select for this specific document — not the type's default. Muted, desaturated tones work best; avoid vivid primaries. When in doubt, go darker and more neutral.
|
||||||
|
|
||||||
|
**content.json block types:**
|
||||||
|
|
||||||
|
| Block | Usage | Key fields |
|
||||||
|
|---|---|---|
|
||||||
|
| `h1` | Section heading + accent rule | `text` |
|
||||||
|
| `h2` | Subsection heading | `text` |
|
||||||
|
| `h3` | Sub-subsection (bold) | `text` |
|
||||||
|
| `body` | Justified paragraph; supports `<b>` `<i>` markup | `text` |
|
||||||
|
| `bullet` | Unordered list item (• prefix) | `text` |
|
||||||
|
| `numbered` | Ordered list item — counter auto-resets on non-numbered blocks | `text` |
|
||||||
|
| `callout` | Highlighted insight box with accent left bar | `text` |
|
||||||
|
| `table` | Data table — accent header, alternating row tints | `headers`, `rows`, `col_widths`?, `caption`? |
|
||||||
|
| `image` | Embedded image scaled to column width | `path`/`src`, `caption`? |
|
||||||
|
| `figure` | Image with auto-numbered "Figure N:" caption | `path`/`src`, `caption`? |
|
||||||
|
| `code` | Monospace code block with accent left border | `text`, `language`? |
|
||||||
|
| `math` | Display math — LaTeX syntax via matplotlib mathtext | `text`, `label`?, `caption`? |
|
||||||
|
| `chart` | Bar / line / pie chart rendered with matplotlib | `chart_type`, `labels`, `datasets`, `title`?, `x_label`?, `y_label`?, `caption`?, `figure`? |
|
||||||
|
| `flowchart` | Process diagram with nodes + edges via matplotlib | `nodes`, `edges`, `caption`?, `figure`? |
|
||||||
|
| `bibliography` | Numbered reference list with hanging indent | `items` [{id, text}], `title`? |
|
||||||
|
| `divider` | Accent-colored full-width rule | — |
|
||||||
|
| `caption` | Small muted label | `text` |
|
||||||
|
| `pagebreak` | Force a new page | — |
|
||||||
|
| `spacer` | Vertical whitespace | `pt` (default 12) |
|
||||||
|
|
||||||
|
**chart / flowchart schemas:**
|
||||||
|
```json
|
||||||
|
{"type":"chart","chart_type":"bar","labels":["Q1","Q2","Q3","Q4"],
|
||||||
|
"datasets":[{"label":"Revenue","values":[120,145,132,178]}],"caption":"Q results"}
|
||||||
|
|
||||||
|
{"type":"flowchart",
|
||||||
|
"nodes":[{"id":"s","label":"Start","shape":"oval"},
|
||||||
|
{"id":"p","label":"Process","shape":"rect"},
|
||||||
|
{"id":"d","label":"Valid?","shape":"diamond"},
|
||||||
|
{"id":"e","label":"End","shape":"oval"}],
|
||||||
|
"edges":[{"from":"s","to":"p"},{"from":"p","to":"d"},
|
||||||
|
{"from":"d","to":"e","label":"Yes"},{"from":"d","to":"p","label":"No"}]}
|
||||||
|
|
||||||
|
{"type":"bibliography","items":[
|
||||||
|
{"id":"1","text":"Author (Year). Title. Publisher."}]}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route B: FILL
|
||||||
|
|
||||||
|
Fill form fields in an existing PDF without altering layout or design.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Step 1: inspect
|
||||||
|
python3 scripts/fill_inspect.py --input form.pdf
|
||||||
|
|
||||||
|
# Step 2: fill
|
||||||
|
python3 scripts/fill_write.py --input form.pdf --out filled.pdf \
|
||||||
|
--values '{"FirstName": "Jane", "Agree": "true", "Country": "US"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field type | Value format |
|
||||||
|
|---|---|
|
||||||
|
| `text` | Any string |
|
||||||
|
| `checkbox` | `"true"` or `"false"` |
|
||||||
|
| `dropdown` | Must match a choice value from inspect output |
|
||||||
|
| `radio` | Must match a radio value (often starts with `/`) |
|
||||||
|
|
||||||
|
Always run `fill_inspect.py` first to get exact field names.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Route C: REFORMAT
|
||||||
|
|
||||||
|
Parse an existing document → content.json → CREATE pipeline.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/make.sh reformat \
|
||||||
|
--input source.md --title "My Report" --type report --out output.pdf
|
||||||
|
```
|
||||||
|
|
||||||
|
**Supported input formats:** `.md` `.txt` `.pdf` `.json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/make.sh check # verify all deps
|
||||||
|
bash scripts/make.sh fix # auto-install missing deps
|
||||||
|
bash scripts/make.sh demo # build a sample PDF
|
||||||
|
```
|
||||||
|
|
||||||
|
| Tool | Used by | Install |
|
||||||
|
|---|---|---|
|
||||||
|
| Python 3.9+ | all `.py` scripts | system |
|
||||||
|
| `reportlab` | `render_body.py` | `pip install reportlab` |
|
||||||
|
| `pypdf` | fill, merge, reformat | `pip install pypdf` |
|
||||||
|
| Node.js 18+ | `render_cover.js` | system |
|
||||||
|
| `playwright` + Chromium | `render_cover.js` | `npm install -g playwright && npx playwright install chromium` |
|
||||||
381
skills/minimax-pdf/design/design.md
Normal file
381
skills/minimax-pdf/design/design.md
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
# Design System
|
||||||
|
|
||||||
|
The aesthetic layer. Read this before touching any script.
|
||||||
|
This file answers "what should it look like and why."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The one rule
|
||||||
|
|
||||||
|
Every design decision must be **rooted in the document's content and purpose**.
|
||||||
|
Dark teal + cream is not "professional". Serif + beige is not "elegant".
|
||||||
|
A color chosen because it fits the content will always outperform a color chosen
|
||||||
|
because it seems safe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Palette logic
|
||||||
|
|
||||||
|
`palette.py` takes a short content description and outputs `tokens.json`.
|
||||||
|
Here is the reasoning it applies:
|
||||||
|
|
||||||
|
### Mood → base palette
|
||||||
|
|
||||||
|
| Content signal | Mood | Background | Accent | Text |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Research, science, analysis | Authoritative | `#0F1F2E` deep ink | `#00B4A6` teal | `#F0EDE6` warm white |
|
||||||
|
| Business, strategy, finance | Confident | `#1C1C2B` near-black | `#E8A020` amber | `#F5F2EC` cream |
|
||||||
|
| Creative, portfolio, design | Expressive | `#1A0A2E` deep violet | `#FF6B6B` coral | `#FAF5FF` lavender white |
|
||||||
|
| Education, academic paper | Scholarly | `#FAFAF7` warm white | `#2C4A7C` navy | `#1A1A2E` dark |
|
||||||
|
| Healthcare, wellness | Calm | `#F5F9F8` pale mint | `#2D8B72` forest | `#1E3830` deep green |
|
||||||
|
| Resume / personal | Clean | `#FFFFFF` white | pick from content | `#111111` near-black |
|
||||||
|
| General / unknown | Neutral | `#F8F6F1` warm off-white | `#3D3D3D` dark gray | `#1A1A1A` black |
|
||||||
|
| Formal publications, annual reports | Magazine | `#F2F0EC` warm linen | `#1C3557` deep navy | `#0D1A2B` near-black |
|
||||||
|
| Premium/dark reports, tech reviews | Darkroom | `#151C27` deep navy | `#4A6FA5` steel blue | `#F0EDE6` warm white |
|
||||||
|
| Technical docs, developer reports | Terminal | `#0D1117` near-black | `#39D353` neon green | `#E6EDF3` cool white |
|
||||||
|
| Portfolios, creative, photography | Poster | `#FFFFFF` white | `#0A0A0A` near-black | `#0A0A0A` near-black |
|
||||||
|
|
||||||
|
### Accent selection rules
|
||||||
|
|
||||||
|
- **One accent color only.** Using two accents splits visual energy.
|
||||||
|
- Accent appears on: cover geometric elements, section rules, callout left borders,
|
||||||
|
table header background, page header rule. Nowhere else.
|
||||||
|
- Accent must contrast with the cover background by at least 4.5:1 (WCAG AA).
|
||||||
|
- Do not default to blue. Blue is the most overused accent in AI-generated documents.
|
||||||
|
|
||||||
|
### Color pairing anti-patterns (never use these)
|
||||||
|
|
||||||
|
| ❌ Avoid | Why |
|
||||||
|
|---|---|
|
||||||
|
| Purple gradient on white | The default AI aesthetic — immediately signals "generated" |
|
||||||
|
| Navy + gold | Overused corporate cliché |
|
||||||
|
| All-black background | Prints badly, feels aggressive |
|
||||||
|
| More than 3 colors in the system | Visual noise |
|
||||||
|
| Accent on body text | Destroys readability |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typography system
|
||||||
|
|
||||||
|
### Font pairing logic
|
||||||
|
|
||||||
|
Two typefaces maximum. Always.
|
||||||
|
|
||||||
|
| Role | Criteria | Good choices (system-safe) |
|
||||||
|
|---|---|---|
|
||||||
|
| Display (cover title, H1) | Distinctive, strong contrast, high weight | Times New Roman, Georgia (serif) |
|
||||||
|
| Text (body, captions, UI) | Highly readable at 10–11pt | Helvetica, Arial (sans) |
|
||||||
|
|
||||||
|
Cover fonts are loaded live via `@import url(...)` in the cover HTML — Playwright
|
||||||
|
fetches them at render time, no local caching. Body pages always use system fonts
|
||||||
|
(Times-Bold / Helvetica) via ReportLab — consistent and offline-safe.
|
||||||
|
|
||||||
|
Pairs by mood (cover HTML only — body always uses system fonts):
|
||||||
|
- Authoritative: `Playfair Display` / `IBM Plex Sans`
|
||||||
|
- Confident: `Syne` / `Nunito Sans`
|
||||||
|
- Expressive: `Fraunces` / `Inter`
|
||||||
|
- Scholarly: `EB Garamond` / `Source Sans 3`
|
||||||
|
- Clean: `DM Serif Display` / `DM Sans`
|
||||||
|
- Restrained: `Cormorant Garamond` / `Jost`
|
||||||
|
- Bold: `Barlow Condensed` / `Barlow`
|
||||||
|
- Dynamic: `Montserrat` / `Montserrat`
|
||||||
|
- Classical: `Cormorant` / `Crimson Pro`
|
||||||
|
- Editorial: `Bebas Neue` / `Libre Franklin`
|
||||||
|
- Body fallback (always): `Times-Bold` / `Helvetica` (ReportLab system fonts)
|
||||||
|
|
||||||
|
### Type scale
|
||||||
|
|
||||||
|
All sizes in points. This scale is used by `palette.py` to populate `tokens.json`.
|
||||||
|
|
||||||
|
| Token | Size | Leading | Usage |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `display` | 54pt | 1.0 | Cover title |
|
||||||
|
| `h1` | 22pt | 1.3 | Section headings |
|
||||||
|
| `h2` | 15pt | 1.4 | Subsection headings |
|
||||||
|
| `h3` | 11.5pt | 1.5 | Sub-subsection |
|
||||||
|
| `body` | 10.5pt | 1.6 | Main prose |
|
||||||
|
| `caption` | 8.5pt | 1.4 | Figure/table captions |
|
||||||
|
| `meta` | 8pt | 1.3 | Header/footer text |
|
||||||
|
|
||||||
|
### Spacing system
|
||||||
|
|
||||||
|
Margins and rhythm are what separate "looks designed" from "looks printed".
|
||||||
|
|
||||||
|
| Token | Value | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `margin_outer` | 2.8cm | Left/right page margin |
|
||||||
|
| `margin_top` | 2.8cm | Top page margin |
|
||||||
|
| `margin_bottom` | 2.5cm | Bottom page margin |
|
||||||
|
| `section_gap` | 26pt | Space before H1 |
|
||||||
|
| `para_gap` | 8pt | Space after paragraph |
|
||||||
|
| `line_gap` | 17pt | Leading for body text |
|
||||||
|
|
||||||
|
Never use ReportLab's default margins (too tight). Always set explicitly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cover design
|
||||||
|
|
||||||
|
The cover is the most important page. It determines whether a reader trusts the document.
|
||||||
|
|
||||||
|
### Thirteen cover patterns
|
||||||
|
|
||||||
|
`cover.py` selects one based on `tokens.json["cover_pattern"]`.
|
||||||
|
|
||||||
|
**1. `fullbleed`** — used for: `report`, `general`
|
||||||
|
- Deep background fills 100% of page
|
||||||
|
- Title: large, left-aligned, upper 60% of page
|
||||||
|
- Accent: thin horizontal rule + top-right corner strip
|
||||||
|
- Dot-grid background texture (subtle, 8–10% opacity)
|
||||||
|
- Footer band: author + date metadata
|
||||||
|
- Fonts: Playfair Display / IBM Plex Sans
|
||||||
|
|
||||||
|
**2. `split`** — used for: `proposal`
|
||||||
|
- Left 42% panel: solid cover color, title + author
|
||||||
|
- Right 58%: off-white, dot-grid decoration
|
||||||
|
- Hard vertical dividing line in accent color
|
||||||
|
- No gradients — pure flat geometry
|
||||||
|
- Fonts: Syne / Nunito Sans
|
||||||
|
|
||||||
|
**3. `typographic`** — used for: `resume`, `academic`
|
||||||
|
- White/off-white background
|
||||||
|
- Name or title as oversized display type (60–80pt), left-aligned
|
||||||
|
- First word in accent color, remainder in dark
|
||||||
|
- Thin rule below title block
|
||||||
|
- Fonts: DM Serif Display / DM Sans (resume) · EB Garamond / Source Sans 3 (academic)
|
||||||
|
|
||||||
|
**4. `atmospheric`** — used for: `portfolio`
|
||||||
|
- Near-black background
|
||||||
|
- Soft radial glow in accent color (upper-right quadrant)
|
||||||
|
- Title centered-left, 2 lines max
|
||||||
|
- Short rule in accent below title
|
||||||
|
- Dot-grid texture at low opacity
|
||||||
|
- Fonts: Fraunces / Inter
|
||||||
|
|
||||||
|
**5. `minimal`** — used for: `minimal`
|
||||||
|
- Near-white background, 8px left accent bar is the only color
|
||||||
|
- Title in very large, light-weight display type (300 weight)
|
||||||
|
- Hairline rule, author + date as single muted line
|
||||||
|
- Nothing else — the bar does all the visual work
|
||||||
|
- Fonts: Cormorant Garamond / Jost
|
||||||
|
|
||||||
|
**6. `stripe`** — used for: `stripe`
|
||||||
|
- Page cut into three horizontal bands: accent / dark / light
|
||||||
|
- Top band: category label; middle: oversized title in white; bottom: metadata
|
||||||
|
- Hard edges, no gradients, no textures — newspaper / brand poster aesthetic
|
||||||
|
- Fonts: Barlow Condensed / Barlow
|
||||||
|
|
||||||
|
**7. `diagonal`** — used for: `diagonal`
|
||||||
|
- SVG polygon cuts page diagonally: dark upper-left, light lower-right
|
||||||
|
- Accent-colored edge line traces the diagonal cut
|
||||||
|
- Title on dark area, metadata on light area
|
||||||
|
- Fonts: Montserrat / Montserrat
|
||||||
|
|
||||||
|
**8. `frame`** — used for: `frame`
|
||||||
|
- White/cream background with an inset rectangular border (1.2px, 28px from edges)
|
||||||
|
- Accent strips inside top + bottom of frame; small accent corner squares
|
||||||
|
- Title centered in the frame space, centered alignment, classical weight
|
||||||
|
- Formal, timeless — annual reports, legal documents, academic papers
|
||||||
|
- Fonts: Cormorant / Crimson Pro
|
||||||
|
|
||||||
|
**9. `editorial`** — used for: `editorial`
|
||||||
|
- Ghost first-letter of title fills upper-right at 5% opacity — visual texture
|
||||||
|
- 5px accent top bar; full-width uppercase title in condensed weight
|
||||||
|
- Title all-caps, very large (80px), flush-left
|
||||||
|
- Footer rule + author/date metadata
|
||||||
|
- Fonts: Bebas Neue / Libre Franklin
|
||||||
|
|
||||||
|
**10. `magazine`** — used for: `magazine`
|
||||||
|
- Warm cream/linen background; fully centered, vertical stack layout
|
||||||
|
- Org/company name in small spaced caps + 2px accent rule beneath (top anchor)
|
||||||
|
- Large bold serif title (52px) centered; short accent rule under title
|
||||||
|
- Italic subtitle; optional `cover_image` URL renders as centered hero thumbnail
|
||||||
|
- Optional `abstract` field: justified text block with bold "Abstract:" label
|
||||||
|
- Author name in accent color (large, bold); date beneath
|
||||||
|
- Fonts: Playfair Display / EB Garamond
|
||||||
|
|
||||||
|
**11. `darkroom`** — used for: `darkroom`
|
||||||
|
- Same centered stack layout as `magazine` but deep navy background, white text
|
||||||
|
- Org name + rules in semi-transparent white; accent rules desaturated
|
||||||
|
- Hero image (if provided) gets `grayscale(20%) brightness(0.9)` filter
|
||||||
|
- Fonts: Playfair Display / EB Garamond
|
||||||
|
|
||||||
|
**12. `terminal`** — used for: `terminal`
|
||||||
|
- Near-black background; neon green accent; Space Mono monospace throughout
|
||||||
|
- Grid overlay: faint horizontal + vertical lines at 48px intervals (7% opacity)
|
||||||
|
- Status label top-left: green dot + `SYSTEM_REPORT // <date>`
|
||||||
|
- Title inside a bracket frame (border-left + border-top + pseudo-element corner)
|
||||||
|
- Subtitle prefixed with `>` in accent color
|
||||||
|
- Abstract text left; author block right; status bar at bottom (UTF-8 / Ln 1)
|
||||||
|
- Fonts: Space Mono / Space Mono
|
||||||
|
|
||||||
|
**13. `poster`** — used for: `poster`
|
||||||
|
- White background; thick 52px left sidebar in accent (typically near-black)
|
||||||
|
- Title: 96px, 900-weight, all-caps, condensed — the dominant visual element
|
||||||
|
- Subtitle in typewriter font below title; thin 2px rule as separator
|
||||||
|
- Author + meta in Courier Prime monospace beneath rule
|
||||||
|
- Optional `cover_image` rendered as 260×340 grayscale thumbnail, right-aligned
|
||||||
|
- Accent square icon block (lower-right) with white horizontal lines
|
||||||
|
- Fonts: Barlow Condensed / Courier Prime
|
||||||
|
|
||||||
|
### Optional token: `cover_image`
|
||||||
|
|
||||||
|
Patterns `magazine`, `darkroom`, and `poster` accept an optional `cover_image`
|
||||||
|
token containing an absolute URL or `file://` path to an image.
|
||||||
|
The image renders via `<img src="...">` — Playwright fetches it at render time.
|
||||||
|
If omitted, the image area is simply skipped (layout adjusts gracefully).
|
||||||
|
|
||||||
|
### Cover CSS requirements (critical for Playwright rendering)
|
||||||
|
|
||||||
|
These three rules must appear in every cover HTML file or the output will have
|
||||||
|
white borders / incorrect dimensions:
|
||||||
|
|
||||||
|
```css
|
||||||
|
body { margin: 0; padding: 0; }
|
||||||
|
html, body { width: 794px; height: 1123px; overflow: hidden; }
|
||||||
|
```
|
||||||
|
|
||||||
|
No `@page` rules needed — Playwright handles page size via the `pdf()` call.
|
||||||
|
Do NOT use CSS `background-image` for textures — use inline SVG or `<canvas>`.
|
||||||
|
Always use `position: absolute` + `z-index` for layered elements.
|
||||||
|
|
||||||
|
### What always kills a cover
|
||||||
|
|
||||||
|
- Centered title on white background with a thin horizontal line underneath
|
||||||
|
- Gradient from one color to another (reads as PowerPoint, not print design)
|
||||||
|
- Drop shadows on text
|
||||||
|
- More than one accent color
|
||||||
|
- Emoji or icon fonts (fail silently on headless Chromium)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inner page rules
|
||||||
|
|
||||||
|
### What "restraint" means in practice
|
||||||
|
|
||||||
|
Every design decision should remove something, not add something.
|
||||||
|
The page is done when there is nothing left to remove.
|
||||||
|
|
||||||
|
- Accent color appears on section rules only — not on headings, not on bullets
|
||||||
|
- No card components (bordered boxes with colored headers)
|
||||||
|
- No rounded corners on anything except callout boxes (4px max)
|
||||||
|
- No shadows anywhere
|
||||||
|
- Tables: header row in accent, alternating row tint, no grid lines except outer box
|
||||||
|
- Callout boxes: left border in accent (4px), very light tint background, no icon
|
||||||
|
|
||||||
|
### Page header / footer
|
||||||
|
|
||||||
|
Header: document title (left, 7.5pt, muted) + accent rule (1.5pt, full width below)
|
||||||
|
Footer: author name (left, 7.5pt, muted) + page number (right, 7.5pt, muted) + light rule above
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quality bar
|
||||||
|
|
||||||
|
A PDF passes if a designer would not be embarrassed to hand it to a client.
|
||||||
|
Concretely:
|
||||||
|
|
||||||
|
- Cover has a clear visual identity that is not "generic AI output"
|
||||||
|
- Body text is readable at arm's length without squinting
|
||||||
|
- Every page looks like it belongs to the same document
|
||||||
|
- No element bleeds off the edge or overlaps another
|
||||||
|
- Page numbers are present and correct
|
||||||
|
- The accent color appears fewer than 8 times per page on average
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Block type reference
|
||||||
|
|
||||||
|
All body blocks use the same token system — colors and fonts come from `tokens.json`, never hardcoded.
|
||||||
|
|
||||||
|
| Block | Rendering | Design notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `h1` | 22pt heading + full-width accent rule below | KeepTogether with rule — heading never orphaned |
|
||||||
|
| `h2` | 15pt heading, dark text | No rule, no accent — visual hierarchy through size only |
|
||||||
|
| `h3` | 11.5pt bold, dark text | **No accent color** — accent on body headings violates the one-accent-location rule |
|
||||||
|
| `body` | 10.5pt justified, 17pt leading | Supports `<b>` `<i>` `<font>` markup |
|
||||||
|
| `bullet` | Body size with `•` prefix, 14pt indent | Use for unordered lists |
|
||||||
|
| `numbered` | Body size with `N.` prefix, hanging indent | Counter auto-resets on any non-numbered block — no manual numbering needed |
|
||||||
|
| `callout` | Accent left-border (4px) + light tint background | Max one callout per section — overuse kills impact |
|
||||||
|
| `table` | Accent header row, alternating row tint, outer box only | Supports `col_widths` (fractions, e.g. `[0.3, 0.5, 0.2]`) for custom column widths |
|
||||||
|
| `image` | Scaled to column width, preserving aspect ratio | Use `path` or `src`; always provide a `caption` |
|
||||||
|
| `figure` | Same as image, but caption auto-prefixed "Figure N:" | Figure counter increments across all `figure`, `chart`, `flowchart` blocks |
|
||||||
|
| `code` | Courier 8.5pt, accent left-border, light tint background | Supports optional `language` label (rendered above block) |
|
||||||
|
| `math` | Formula centered, optional right-aligned equation label | LaTeX syntax; matplotlib mathtext renderer |
|
||||||
|
| `chart` | Bar / line / pie chart rendered via matplotlib | Color palette derived from document accent; figure auto-numbered |
|
||||||
|
| `flowchart` | Process diagram with labeled arrows | Supports 4 node shapes; back-edges drawn as curved arcs |
|
||||||
|
| `bibliography` | Numbered reference list with hanging indent | Heading rendered as h2 + accent rule; items as `[N] text` |
|
||||||
|
| `divider` | Accent-colored 1.2pt rule with padding | Use sparingly — only for major thematic breaks |
|
||||||
|
| `caption` | 8.5pt muted text, centered | Appears below images/tables via field or explicit block |
|
||||||
|
| `pagebreak` | Force page break | — |
|
||||||
|
| `spacer` | Vertical whitespace | `pt` field (default 12) |
|
||||||
|
|
||||||
|
### Math formula guidance
|
||||||
|
|
||||||
|
**Input syntax:** standard LaTeX math notation — `\frac{}{}`, `\int`, `\sum`, `\alpha`, `^`, `_`, etc.
|
||||||
|
**Rendering engine:** matplotlib mathtext — pure Python, no LaTeX compiler, no browser required.
|
||||||
|
|
||||||
|
| Syntax example | Rendered as |
|
||||||
|
|---|---|
|
||||||
|
| `E = mc^2` | Inline expression |
|
||||||
|
| `\frac{\sqrt{\pi}}{2}` | Fraction |
|
||||||
|
| `\int_0^\infty e^{-x^2} dx` | Integral |
|
||||||
|
| `\sum_{i=1}^{n} x_i` | Summation |
|
||||||
|
| `\alpha + \beta = \gamma` | Greek letters |
|
||||||
|
|
||||||
|
**Limitations:** matplotlib mathtext covers most common expressions but not advanced LaTeX environments (`align`, `cases`, `matrix`). Split complex multi-line proofs into multiple `math` blocks.
|
||||||
|
|
||||||
|
**Fallback:** if matplotlib is not installed, renders as `expression` in code style. Run `make.sh fix` to install.
|
||||||
|
|
||||||
|
**Equation labels:** `"label": "(1)"` — rendered right-aligned beside the formula.
|
||||||
|
|
||||||
|
### Chart guidance
|
||||||
|
|
||||||
|
**Rendered entirely in Python** — no external chart services, image files, or internet required.
|
||||||
|
|
||||||
|
| chart_type | Use case | Required fields |
|
||||||
|
|---|---|---|
|
||||||
|
| `bar` | Comparing discrete categories | `labels`, `datasets` |
|
||||||
|
| `line` | Trends over time or ordered categories | `labels`, `datasets` |
|
||||||
|
| `pie` | Part-to-whole composition | `labels`, `datasets[0].values` |
|
||||||
|
|
||||||
|
- Colors are derived from the document accent for visual consistency — do not set custom colors.
|
||||||
|
- Multi-series: add multiple objects to `datasets`, each with a `label` and `values` array.
|
||||||
|
- Figure auto-numbering: set `"figure": true` (default) or `"figure": false` to suppress.
|
||||||
|
|
||||||
|
### Flowchart guidance
|
||||||
|
|
||||||
|
**Node shapes:**
|
||||||
|
|
||||||
|
| shape | Use for |
|
||||||
|
|---|---|
|
||||||
|
| `rect` (default) | Process step |
|
||||||
|
| `diamond` | Decision / condition |
|
||||||
|
| `oval` or `terminal` | Start / End |
|
||||||
|
| `parallelogram` | Input / Output |
|
||||||
|
|
||||||
|
- Nodes are placed in input order (top to bottom). This controls the layout.
|
||||||
|
- Forward edges draw straight arrows; back-edges (to earlier nodes) draw curved arcs.
|
||||||
|
- Keep labels short (3–5 words max) — the diagram is A4-column-width at 78% scale.
|
||||||
|
- Figure auto-numbering applies same as chart.
|
||||||
|
|
||||||
|
### Bibliography guidance
|
||||||
|
|
||||||
|
- `id` field is the reference label — use numbers ("1", "2") or alphanumeric ("Smith23").
|
||||||
|
- Text should be in a consistent citation style (APA, Chicago, etc.) — the renderer does not enforce style.
|
||||||
|
- The `title` field defaults to "References". Set `"title": ""` to suppress the heading.
|
||||||
|
- A `bibliography` block always starts with a new section heading + accent rule.
|
||||||
|
|
||||||
|
### Image / figure guidance
|
||||||
|
|
||||||
|
- Preferred formats: PNG, JPEG
|
||||||
|
- Scaled down if wider than the text column; never scaled up
|
||||||
|
- `figure` blocks auto-number; `image` blocks do not — use `figure` for numbered figures
|
||||||
|
- If the file does not exist at render time, a `[Image not found]` placeholder is substituted
|
||||||
|
|
||||||
|
### Code block guidance
|
||||||
|
|
||||||
|
- Preserves whitespace exactly — do not indent code in the JSON value
|
||||||
|
- Optional `language` field renders a small language label above the block (e.g., `"language": "python"`)
|
||||||
|
- No syntax highlighting (by design) — consistent with restraint principle
|
||||||
|
- Keep lines under ~90 characters for A4 column width
|
||||||
1579
skills/minimax-pdf/scripts/cover.py
Normal file
1579
skills/minimax-pdf/scripts/cover.py
Normal file
File diff suppressed because it is too large
Load Diff
200
skills/minimax-pdf/scripts/fill_inspect.py
Normal file
200
skills/minimax-pdf/scripts/fill_inspect.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fill_inspect.py — Inspect form fields in an existing PDF.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 fill_inspect.py --input form.pdf
|
||||||
|
python3 fill_inspect.py --input form.pdf --out fields.json
|
||||||
|
|
||||||
|
Outputs a JSON summary of every fillable field: name, type, current value,
|
||||||
|
allowed values (for checkboxes / dropdowns), and page number.
|
||||||
|
|
||||||
|
Exit codes: 0 success, 1 bad args / file not found, 2 dep missing, 3 read error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_deps():
|
||||||
|
if importlib.util.find_spec("pypdf") is None:
|
||||||
|
import subprocess
|
||||||
|
subprocess.check_call(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--break-system-packages", "-q", "pypdf"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ensure_deps()
|
||||||
|
from pypdf import PdfReader
|
||||||
|
from pypdf.generic import ArrayObject, DictionaryObject, NameObject, TextStringObject
|
||||||
|
|
||||||
|
|
||||||
|
# ── Field type resolution ──────────────────────────────────────────────────────
|
||||||
|
def _field_type(field) -> str:
|
||||||
|
ft = field.get("/FT")
|
||||||
|
if ft is None:
|
||||||
|
return "unknown"
|
||||||
|
ft = str(ft)
|
||||||
|
if ft == "/Tx":
|
||||||
|
return "text"
|
||||||
|
if ft == "/Btn":
|
||||||
|
ff = int(field.get("/Ff", 0))
|
||||||
|
return "radio" if ff & (1 << 15) else "checkbox"
|
||||||
|
if ft == "/Ch":
|
||||||
|
ff = int(field.get("/Ff", 0))
|
||||||
|
return "dropdown" if ff & (1 << 17) else "listbox"
|
||||||
|
if ft == "/Sig":
|
||||||
|
return "signature"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _field_value(field) -> str | None:
|
||||||
|
v = field.get("/V")
|
||||||
|
return str(v) if v is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _field_options(field, ftype: str) -> dict:
|
||||||
|
extra = {}
|
||||||
|
if ftype in ("checkbox",):
|
||||||
|
ap = field.get("/AP")
|
||||||
|
if ap and "/N" in ap:
|
||||||
|
states = [str(k) for k in ap["/N"]]
|
||||||
|
extra["states"] = states
|
||||||
|
checked = next((s for s in states if s != "/Off"), None)
|
||||||
|
if checked:
|
||||||
|
extra["checked_value"] = checked
|
||||||
|
if ftype in ("dropdown", "listbox"):
|
||||||
|
opt = field.get("/Opt")
|
||||||
|
if opt:
|
||||||
|
choices = []
|
||||||
|
for item in opt:
|
||||||
|
if isinstance(item, (list, ArrayObject)) and len(item) >= 2:
|
||||||
|
choices.append({"value": str(item[0]), "label": str(item[1])})
|
||||||
|
else:
|
||||||
|
choices.append({"value": str(item), "label": str(item)})
|
||||||
|
extra["choices"] = choices
|
||||||
|
if ftype == "radio":
|
||||||
|
kids = field.get("/Kids")
|
||||||
|
if kids:
|
||||||
|
values = []
|
||||||
|
for kid in kids:
|
||||||
|
ap = kid.get("/AP")
|
||||||
|
if ap and "/N" in ap:
|
||||||
|
for k in ap["/N"]:
|
||||||
|
if str(k) != "/Off":
|
||||||
|
values.append(str(k))
|
||||||
|
extra["radio_values"] = values
|
||||||
|
return extra
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_fields(fields, page_map: dict, parent_name: str = "") -> list:
|
||||||
|
"""Recursively collect all leaf fields."""
|
||||||
|
result = []
|
||||||
|
for field in fields:
|
||||||
|
name = str(field.get("/T", ""))
|
||||||
|
full = f"{parent_name}.{name}" if parent_name else name
|
||||||
|
|
||||||
|
kids = field.get("/Kids")
|
||||||
|
# Kids that have /T are sub-fields (groups), not widget annotations
|
||||||
|
if kids:
|
||||||
|
named_kids = [k for k in kids if "/T" in k]
|
||||||
|
if named_kids:
|
||||||
|
result.extend(_walk_fields(named_kids, page_map, full))
|
||||||
|
continue
|
||||||
|
|
||||||
|
ftype = _field_type(field)
|
||||||
|
if ftype == "unknown":
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
"name": full,
|
||||||
|
"type": ftype,
|
||||||
|
"value": _field_value(field),
|
||||||
|
}
|
||||||
|
entry.update(_field_options(field, ftype))
|
||||||
|
|
||||||
|
# Page lookup via /P indirect reference
|
||||||
|
p_ref = field.get("/P")
|
||||||
|
if p_ref and hasattr(p_ref, "idnum"):
|
||||||
|
entry["page"] = page_map.get(p_ref.idnum, "?")
|
||||||
|
|
||||||
|
result.append(entry)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def inspect(pdf_path: str) -> dict:
|
||||||
|
try:
|
||||||
|
reader = PdfReader(pdf_path)
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
# Build page-number lookup: {object_id: 1-based page number}
|
||||||
|
page_map = {}
|
||||||
|
for i, page in enumerate(reader.pages):
|
||||||
|
if hasattr(page, "indirect_reference") and page.indirect_reference:
|
||||||
|
page_map[page.indirect_reference.idnum] = i + 1
|
||||||
|
|
||||||
|
acroform = reader.trailer.get("/Root", {}).get("/AcroForm")
|
||||||
|
if acroform is None or "/Fields" not in acroform:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"has_fields": False,
|
||||||
|
"field_count": 0,
|
||||||
|
"fields": [],
|
||||||
|
"note": "This PDF has no fillable form fields.",
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = _walk_fields(list(acroform["/Fields"]), page_map)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"has_fields": bool(fields),
|
||||||
|
"field_count": len(fields),
|
||||||
|
"fields": fields,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Inspect PDF form fields")
|
||||||
|
parser.add_argument("--input", required=True, help="PDF file to inspect")
|
||||||
|
parser.add_argument("--out", default="", help="Write JSON to file (optional)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
print(json.dumps({"status": "error", "error": f"File not found: {args.input}"}),
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
result = inspect(args.input)
|
||||||
|
|
||||||
|
output = json.dumps(result, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
if args.out:
|
||||||
|
with open(args.out, "w") as f:
|
||||||
|
f.write(output)
|
||||||
|
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
# Human-readable summary
|
||||||
|
if result["status"] == "ok" and result["has_fields"]:
|
||||||
|
print(f"\n── Fields in {args.input} ──────────────────────────────",
|
||||||
|
file=sys.stderr)
|
||||||
|
for f in result["fields"]:
|
||||||
|
pg = f" p.{f['page']}" if "page" in f else ""
|
||||||
|
val = f" = {f['value']}" if f.get("value") else ""
|
||||||
|
extra = ""
|
||||||
|
if "choices" in f:
|
||||||
|
extra = f" [{', '.join(c['value'] for c in f['choices'][:4])}{'…' if len(f['choices'])>4 else ''}]"
|
||||||
|
elif "states" in f:
|
||||||
|
extra = f" {f['states']}"
|
||||||
|
print(f" {f['type']:12} {f['name']}{pg}{val}{extra}", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
242
skills/minimax-pdf/scripts/fill_write.py
Normal file
242
skills/minimax-pdf/scripts/fill_write.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fill_write.py — Write values into PDF form fields.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# From a JSON data file
|
||||||
|
python3 fill_write.py --input form.pdf --data values.json --out filled.pdf
|
||||||
|
|
||||||
|
# Inline JSON
|
||||||
|
python3 fill_write.py --input form.pdf --out filled.pdf \
|
||||||
|
--values '{"FirstName": "Jane", "Agree": "true"}'
|
||||||
|
|
||||||
|
values format:
|
||||||
|
{
|
||||||
|
"FieldName": "text value", # text field
|
||||||
|
"CheckBox1": "true", # checkbox (true / false)
|
||||||
|
"Dropdown1": "OptionValue", # dropdown (must match an existing choice value)
|
||||||
|
"Radio1": "/Choice2" # radio (must match a radio value)
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit codes: 0 success, 1 bad args, 2 dep missing, 3 read/write error, 4 validation error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_deps():
|
||||||
|
if importlib.util.find_spec("pypdf") is None:
|
||||||
|
import subprocess
|
||||||
|
subprocess.check_call(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--break-system-packages", "-q", "pypdf"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ensure_deps()
|
||||||
|
from pypdf import PdfReader, PdfWriter
|
||||||
|
from pypdf.generic import NameObject, TextStringObject, BooleanObject
|
||||||
|
|
||||||
|
|
||||||
|
# ── Field helpers ─────────────────────────────────────────────────────────────
|
||||||
|
def _field_type(field) -> str:
|
||||||
|
ft = str(field.get("/FT", ""))
|
||||||
|
if ft == "/Tx": return "text"
|
||||||
|
if ft == "/Btn":
|
||||||
|
ff = int(field.get("/Ff", 0))
|
||||||
|
return "radio" if ff & (1 << 15) else "checkbox"
|
||||||
|
if ft == "/Ch":
|
||||||
|
ff = int(field.get("/Ff", 0))
|
||||||
|
return "dropdown" if ff & (1 << 17) else "listbox"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_checkbox_on_value(field) -> str:
|
||||||
|
"""Return the /AP /N key that means 'checked' (anything except /Off)."""
|
||||||
|
ap = field.get("/AP")
|
||||||
|
if ap and "/N" in ap:
|
||||||
|
for k in ap["/N"]:
|
||||||
|
if str(k) != "/Off":
|
||||||
|
return str(k)
|
||||||
|
return "/Yes"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dropdown_values(field) -> list[str]:
|
||||||
|
opt = field.get("/Opt")
|
||||||
|
if not opt:
|
||||||
|
return []
|
||||||
|
values = []
|
||||||
|
for item in opt:
|
||||||
|
try:
|
||||||
|
from pypdf.generic import ArrayObject
|
||||||
|
if isinstance(item, (list, ArrayObject)) and len(item) >= 1:
|
||||||
|
values.append(str(item[0]))
|
||||||
|
else:
|
||||||
|
values.append(str(item))
|
||||||
|
except Exception:
|
||||||
|
values.append(str(item))
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
# ── Walk + fill ───────────────────────────────────────────────────────────────
|
||||||
|
def _walk_and_fill(fields, data: dict, filled: list, errors: list, parent: str = ""):
|
||||||
|
for field in fields:
|
||||||
|
name = str(field.get("/T", ""))
|
||||||
|
full = f"{parent}.{name}" if parent else name
|
||||||
|
|
||||||
|
# Recurse into named groups
|
||||||
|
kids = field.get("/Kids")
|
||||||
|
if kids:
|
||||||
|
named = [k for k in kids if "/T" in k]
|
||||||
|
if named:
|
||||||
|
_walk_and_fill(named, data, filled, errors, full)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if full not in data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = data[full]
|
||||||
|
ftype = _field_type(field)
|
||||||
|
|
||||||
|
if ftype == "text":
|
||||||
|
field.update({
|
||||||
|
NameObject("/V"): TextStringObject(str(value)),
|
||||||
|
NameObject("/DV"): TextStringObject(str(value)),
|
||||||
|
})
|
||||||
|
filled.append(full)
|
||||||
|
|
||||||
|
elif ftype == "checkbox":
|
||||||
|
truthy = str(value).lower() in ("true", "1", "yes", "on")
|
||||||
|
on_val = _get_checkbox_on_value(field)
|
||||||
|
pdf_val = on_val if truthy else "/Off"
|
||||||
|
field.update({
|
||||||
|
NameObject("/V"): NameObject(pdf_val),
|
||||||
|
NameObject("/AS"): NameObject(pdf_val),
|
||||||
|
})
|
||||||
|
filled.append(full)
|
||||||
|
|
||||||
|
elif ftype in ("dropdown", "listbox"):
|
||||||
|
allowed = _get_dropdown_values(field)
|
||||||
|
if allowed and str(value) not in allowed:
|
||||||
|
errors.append({
|
||||||
|
"field": full,
|
||||||
|
"error": f"Value '{value}' not in allowed choices: {allowed}"
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
field.update({NameObject("/V"): TextStringObject(str(value))})
|
||||||
|
filled.append(full)
|
||||||
|
|
||||||
|
elif ftype == "radio":
|
||||||
|
# Radio value must start with /
|
||||||
|
pdf_val = str(value) if str(value).startswith("/") else f"/{value}"
|
||||||
|
field.update({
|
||||||
|
NameObject("/V"): NameObject(pdf_val),
|
||||||
|
NameObject("/AS"): NameObject(pdf_val),
|
||||||
|
})
|
||||||
|
filled.append(full)
|
||||||
|
|
||||||
|
else:
|
||||||
|
errors.append({"field": full, "error": f"Unsupported field type: {ftype}"})
|
||||||
|
|
||||||
|
|
||||||
|
def fill(pdf_path: str, out_path: str, data: dict) -> dict:
|
||||||
|
try:
|
||||||
|
reader = PdfReader(pdf_path)
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
writer = PdfWriter()
|
||||||
|
writer.clone_document_from_reader(reader)
|
||||||
|
|
||||||
|
acroform = writer._root_object.get("/AcroForm") # type: ignore[attr-defined]
|
||||||
|
if acroform is None or "/Fields" not in acroform:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "This PDF has no fillable form fields.",
|
||||||
|
"hint": "Run fill_inspect.py first to confirm the PDF has fields.",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Enable appearance regeneration so viewers show the new values
|
||||||
|
acroform.update({NameObject("/NeedAppearances"): BooleanObject(True)})
|
||||||
|
|
||||||
|
filled: list[str] = []
|
||||||
|
errors: list[dict] = []
|
||||||
|
_walk_and_fill(list(acroform["/Fields"]), data, filled, errors)
|
||||||
|
|
||||||
|
# Warn about requested fields that were never found
|
||||||
|
not_found = [k for k in data if k not in filled and not any(e["field"] == k for e in errors)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "error": f"Write failed: {e}"}
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"out": out_path,
|
||||||
|
"filled_count": len(filled),
|
||||||
|
"filled_fields": filled,
|
||||||
|
"size_kb": os.path.getsize(out_path) // 1024,
|
||||||
|
}
|
||||||
|
if errors:
|
||||||
|
result["validation_errors"] = errors
|
||||||
|
if not_found:
|
||||||
|
result["not_found"] = not_found
|
||||||
|
result["hint"] = "Run fill_inspect.py to see all available field names."
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Fill PDF form fields")
|
||||||
|
parser.add_argument("--input", required=True, help="Input PDF with form fields")
|
||||||
|
parser.add_argument("--out", required=True, help="Output PDF path")
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument("--data", help="Path to JSON file with field values")
|
||||||
|
group.add_argument("--values", help="Inline JSON string with field values")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
print(json.dumps({"status": "error", "error": f"File not found: {args.input}"}),
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Load data
|
||||||
|
try:
|
||||||
|
if args.data:
|
||||||
|
with open(args.data) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
else:
|
||||||
|
data = json.loads(args.values)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"status": "error", "error": f"JSON parse error: {e}"}),
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
result = fill(args.input, args.out, data)
|
||||||
|
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||||
|
|
||||||
|
if result["status"] == "ok":
|
||||||
|
print(f"\n── Fill complete ───────────────────────────────────────",
|
||||||
|
file=sys.stderr)
|
||||||
|
print(f" Output : {result['out']}", file=sys.stderr)
|
||||||
|
print(f" Filled : {result['filled_count']} field(s)", file=sys.stderr)
|
||||||
|
if result.get("validation_errors"):
|
||||||
|
print(f" Errors :", file=sys.stderr)
|
||||||
|
for e in result["validation_errors"]:
|
||||||
|
print(f" • {e['field']}: {e['error']}", file=sys.stderr)
|
||||||
|
if result.get("not_found"):
|
||||||
|
print(f" Not found: {result['not_found']}", file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
else:
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
491
skills/minimax-pdf/scripts/make.sh
Normal file
491
skills/minimax-pdf/scripts/make.sh
Normal file
@@ -0,0 +1,491 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# make.sh — minimax-pdf unified CLI
|
||||||
|
# Usage: bash make.sh <command> [options]
|
||||||
|
#
|
||||||
|
# Commands:
|
||||||
|
# check Verify all dependencies
|
||||||
|
# fix Auto-install missing dependencies
|
||||||
|
# run --title T --type TYPE Full pipeline → output.pdf
|
||||||
|
# --out FILE Output path (default: output.pdf)
|
||||||
|
# --author A --date D
|
||||||
|
# --subtitle S
|
||||||
|
# --abstract A Optional abstract text for cover
|
||||||
|
# --cover-image URL Optional cover image URL/path
|
||||||
|
# --content FILE Path to content.json (optional)
|
||||||
|
# demo Build a full-featured demo to demo.pdf
|
||||||
|
#
|
||||||
|
# Document types:
|
||||||
|
# report proposal resume portfolio academic general
|
||||||
|
# minimal stripe diagonal frame editorial
|
||||||
|
# magazine darkroom terminal poster
|
||||||
|
#
|
||||||
|
# Content block types:
|
||||||
|
# h1 h2 h3 body bullet numbered callout table
|
||||||
|
# image figure code math chart flowchart bibliography
|
||||||
|
# divider caption pagebreak spacer
|
||||||
|
#
|
||||||
|
# Exit codes: 0 success, 1 usage error, 2 dep missing, 3 runtime error
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PY="python3"
|
||||||
|
NODE="node"
|
||||||
|
|
||||||
|
# ── Colour helpers ─────────────────────────────────────────────────────────────
|
||||||
|
red() { printf '\033[0;31m%s\033[0m\n' "$*"; }
|
||||||
|
green() { printf '\033[0;32m%s\033[0m\n' "$*"; }
|
||||||
|
yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; }
|
||||||
|
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||||
|
|
||||||
|
# ── check ──────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_check() {
|
||||||
|
local ok=true
|
||||||
|
bold "Checking dependencies..."
|
||||||
|
|
||||||
|
# Python
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
green " ✓ python3 $(python3 --version 2>&1 | awk '{print $2}')"
|
||||||
|
else
|
||||||
|
red " ✗ python3 not found"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# reportlab
|
||||||
|
if python3 -c "import reportlab" 2>/dev/null; then
|
||||||
|
green " ✓ reportlab"
|
||||||
|
else
|
||||||
|
yellow " ⚠ reportlab not installed (run: make.sh fix)"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pypdf
|
||||||
|
if python3 -c "import pypdf" 2>/dev/null; then
|
||||||
|
green " ✓ pypdf"
|
||||||
|
else
|
||||||
|
yellow " ⚠ pypdf not installed (run: make.sh fix)"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
green " ✓ node $(node --version)"
|
||||||
|
else
|
||||||
|
red " ✗ node not found — cover rendering unavailable"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
if node -e "require('playwright')" 2>/dev/null || \
|
||||||
|
node -e "require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright')" 2>/dev/null; then
|
||||||
|
green " ✓ playwright"
|
||||||
|
else
|
||||||
|
yellow " ⚠ playwright not found (run: make.sh fix)"
|
||||||
|
ok=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# matplotlib (optional — required for math/chart/flowchart; degrades gracefully)
|
||||||
|
if python3 -c "import matplotlib" 2>/dev/null; then
|
||||||
|
green " ✓ matplotlib (math, chart, flowchart blocks enabled)"
|
||||||
|
else
|
||||||
|
yellow " ⚠ matplotlib not installed — math/chart/flowchart blocks degrade to text (run: make.sh fix)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $ok; then
|
||||||
|
green "\nAll dependencies satisfied."
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
yellow "\nSome dependencies missing. Run: bash make.sh fix"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fix ────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_fix() {
|
||||||
|
bold "Installing missing dependencies..."
|
||||||
|
local rc=0
|
||||||
|
|
||||||
|
# Python packages
|
||||||
|
if command -v python3 &>/dev/null; then
|
||||||
|
python3 -m pip install --break-system-packages -q reportlab pypdf matplotlib 2>/dev/null \
|
||||||
|
|| python3 -m pip install -q reportlab pypdf matplotlib 2>/dev/null \
|
||||||
|
|| { yellow " pip install failed — try: pip install reportlab pypdf matplotlib"; rc=3; }
|
||||||
|
green " ✓ Python packages installed (reportlab, pypdf, matplotlib)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
if command -v npm &>/dev/null; then
|
||||||
|
npm install -g playwright --silent 2>/dev/null && \
|
||||||
|
npx playwright install chromium --silent 2>/dev/null && \
|
||||||
|
green " ✓ Playwright + Chromium installed" || \
|
||||||
|
{ yellow " playwright install failed — try manually"; rc=3; }
|
||||||
|
else
|
||||||
|
yellow " npm not found — cannot install Playwright automatically"
|
||||||
|
rc=2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
|
green "\nAll dependencies installed. Run: bash make.sh check"
|
||||||
|
fi
|
||||||
|
exit $rc
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── run ────────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_run() {
|
||||||
|
local title="Untitled Document"
|
||||||
|
local type="general"
|
||||||
|
local author=""
|
||||||
|
local date=""
|
||||||
|
local subtitle=""
|
||||||
|
local abstract=""
|
||||||
|
local cover_image=""
|
||||||
|
local accent=""
|
||||||
|
local cover_bg=""
|
||||||
|
local content_file=""
|
||||||
|
local out="output.pdf"
|
||||||
|
local workdir
|
||||||
|
workdir="$(mktemp -d)"
|
||||||
|
|
||||||
|
# Parse options
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--title) title="$2"; shift 2 ;;
|
||||||
|
--type) type="$2"; shift 2 ;;
|
||||||
|
--author) author="$2"; shift 2 ;;
|
||||||
|
--date) date="$2"; shift 2 ;;
|
||||||
|
--subtitle) subtitle="$2"; shift 2 ;;
|
||||||
|
--abstract) abstract="$2"; shift 2 ;;
|
||||||
|
--cover-image) cover_image="$2"; shift 2 ;;
|
||||||
|
--accent) accent="$2"; shift 2 ;;
|
||||||
|
--cover-bg) cover_bg="$2"; shift 2 ;;
|
||||||
|
--content) content_file="$2"; shift 2 ;;
|
||||||
|
--out) out="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
bold "Building: $title"
|
||||||
|
echo " Type : $type"
|
||||||
|
echo " Output : $out"
|
||||||
|
|
||||||
|
# Step 1: tokens
|
||||||
|
echo ""
|
||||||
|
bold "Step 1/4 Generating design tokens..."
|
||||||
|
local accent_args=()
|
||||||
|
[[ -n "$accent" ]] && accent_args+=(--accent "$accent")
|
||||||
|
[[ -n "$cover_bg" ]] && accent_args+=(--cover-bg "$cover_bg")
|
||||||
|
$PY "$SCRIPTS/palette.py" \
|
||||||
|
--title "$title" --type "$type" \
|
||||||
|
--author "$author" --date "$date" \
|
||||||
|
--out "$workdir/tokens.json" \
|
||||||
|
"${accent_args[@]+"${accent_args[@]}"}"
|
||||||
|
|
||||||
|
# Inject optional cover fields into tokens.json
|
||||||
|
if [[ -n "$abstract" || -n "$cover_image" ]]; then
|
||||||
|
PDF_ABSTRACT="$abstract" PDF_COVER_IMAGE="$cover_image" PDF_TOKENS="$workdir/tokens.json" \
|
||||||
|
$PY - <<'PYEOF'
|
||||||
|
import json, os
|
||||||
|
with open(os.environ["PDF_TOKENS"]) as f:
|
||||||
|
t = json.load(f)
|
||||||
|
abstract = os.environ.get("PDF_ABSTRACT", "")
|
||||||
|
cover_image = os.environ.get("PDF_COVER_IMAGE", "")
|
||||||
|
if abstract:
|
||||||
|
t["abstract"] = abstract
|
||||||
|
if cover_image:
|
||||||
|
t["cover_image"] = cover_image
|
||||||
|
with open(os.environ["PDF_TOKENS"], "w") as f:
|
||||||
|
json.dump(t, f, indent=2)
|
||||||
|
PYEOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat "$workdir/tokens.json" | $PY -c "
|
||||||
|
import json,sys
|
||||||
|
t=json.load(sys.stdin)
|
||||||
|
print(f' Mood : {t[\"mood\"]}')
|
||||||
|
print(f' Pattern : {t[\"cover_pattern\"]}')
|
||||||
|
print(f' Fonts : {t[\"font_display\"]} / {t[\"font_body\"]}')"
|
||||||
|
|
||||||
|
# Step 2: cover HTML + render
|
||||||
|
echo ""
|
||||||
|
bold "Step 2/4 Rendering cover..."
|
||||||
|
local subtitle_args=()
|
||||||
|
[[ -n "$subtitle" ]] && subtitle_args=(--subtitle "$subtitle")
|
||||||
|
$PY "$SCRIPTS/cover.py" \
|
||||||
|
--tokens "$workdir/tokens.json" \
|
||||||
|
--out "$workdir/cover.html" \
|
||||||
|
"${subtitle_args[@]+"${subtitle_args[@]}"}"
|
||||||
|
|
||||||
|
$NODE "$SCRIPTS/render_cover.js" \
|
||||||
|
--input "$workdir/cover.html" \
|
||||||
|
--out "$workdir/cover.pdf"
|
||||||
|
green " ✓ Cover rendered"
|
||||||
|
|
||||||
|
# Step 3: body
|
||||||
|
echo ""
|
||||||
|
bold "Step 3/4 Rendering body pages..."
|
||||||
|
if [[ -z "$content_file" ]]; then
|
||||||
|
# Generate a minimal placeholder body
|
||||||
|
cat > "$workdir/content.json" <<'JSON'
|
||||||
|
[
|
||||||
|
{"type":"h1", "text":"Document Body"},
|
||||||
|
{"type":"body", "text":"Replace this with your content.json file using --content path/to/content.json"},
|
||||||
|
{"type":"body", "text":"See the content.json schema in the skill README for the full list of supported block types: h1, h2, h3, body, bullet, callout, table, pagebreak, spacer."}
|
||||||
|
]
|
||||||
|
JSON
|
||||||
|
content_file="$workdir/content.json"
|
||||||
|
yellow " No content file provided — using placeholder body."
|
||||||
|
fi
|
||||||
|
|
||||||
|
$PY "$SCRIPTS/render_body.py" \
|
||||||
|
--tokens "$workdir/tokens.json" \
|
||||||
|
--content "$content_file" \
|
||||||
|
--out "$workdir/body.pdf"
|
||||||
|
green " ✓ Body rendered"
|
||||||
|
|
||||||
|
# Step 4: merge
|
||||||
|
echo ""
|
||||||
|
bold "Step 4/4 Merging and QA..."
|
||||||
|
$PY "$SCRIPTS/merge.py" \
|
||||||
|
--cover "$workdir/cover.pdf" \
|
||||||
|
--body "$workdir/body.pdf" \
|
||||||
|
--out "$out" \
|
||||||
|
--title "$title"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -rf "$workdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── fill ──────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_fill() {
|
||||||
|
local input="" out="" values="" data_file="" inspect_only=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--input) input="$2"; shift 2 ;;
|
||||||
|
--out) out="$2"; shift 2 ;;
|
||||||
|
--values) values="$2"; shift 2 ;;
|
||||||
|
--data) data_file="$2"; shift 2 ;;
|
||||||
|
--inspect) inspect_only=true; shift ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$input" ]]; then
|
||||||
|
echo "Usage: make.sh fill --input form.pdf [--out filled.pdf] [--values '{...}'] [--data values.json] [--inspect]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $inspect_only || [[ -z "$out" && -z "$values" && -z "$data_file" ]]; then
|
||||||
|
bold "Inspecting form fields in: $input"
|
||||||
|
$PY "$SCRIPTS/fill_inspect.py" --input "$input"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
bold "Filling form: $input → $out"
|
||||||
|
|
||||||
|
local val_args=""
|
||||||
|
if [[ -n "$values" ]]; then val_args="--values $values"; fi
|
||||||
|
if [[ -n "$data_file" ]]; then val_args="--data $data_file"; fi
|
||||||
|
|
||||||
|
$PY "$SCRIPTS/fill_write.py" --input "$input" --out "$out" $val_args
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── reformat ───────────────────────────────────────────────────────────────────
|
||||||
|
cmd_reformat() {
|
||||||
|
local input="" title="Reformatted Document" type="general"
|
||||||
|
local author="" date="" out="output.pdf" subtitle=""
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--input) input="$2"; shift 2 ;;
|
||||||
|
--title) title="$2"; shift 2 ;;
|
||||||
|
--type) type="$2"; shift 2 ;;
|
||||||
|
--author) author="$2"; shift 2 ;;
|
||||||
|
--date) date="$2"; shift 2 ;;
|
||||||
|
--subtitle) subtitle="$2"; shift 2 ;;
|
||||||
|
--out) out="$2"; shift 2 ;;
|
||||||
|
*) echo "Unknown option: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$input" ]]; then
|
||||||
|
echo "Usage: make.sh reformat --input source.md --title T --type TYPE --out output.pdf"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
bold "Parsing: $input"
|
||||||
|
$PY "$SCRIPTS/reformat_parse.py" --input "$input" --out "$tmpdir/content.json"
|
||||||
|
green " ✓ Parsed to content.json"
|
||||||
|
|
||||||
|
bold "Applying design and building PDF..."
|
||||||
|
local sub_args=()
|
||||||
|
[[ -n "$subtitle" ]] && sub_args=(--subtitle "$subtitle")
|
||||||
|
|
||||||
|
cmd_run \
|
||||||
|
--title "$title" --type "$type" \
|
||||||
|
--author "$author" --date "$date" \
|
||||||
|
--content "$tmpdir/content.json" \
|
||||||
|
--out "$out" \
|
||||||
|
"${sub_args[@]+"${sub_args[@]}"}"
|
||||||
|
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── demo ──────────────────────────────────────────────────────────────────────
|
||||||
|
cmd_demo() {
|
||||||
|
local tmpdir
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
|
||||||
|
cat > "$tmpdir/content.json" <<'JSON'
|
||||||
|
[
|
||||||
|
{"type":"h1", "text":"Executive Summary"},
|
||||||
|
{"type":"body", "text":"This document was generated by minimax-pdf — a skill for creating visually polished PDFs. Every design decision is rooted in the document type and content, not a generic template."},
|
||||||
|
{"type":"callout", "text":"Key insight: design tokens flow from palette.py through every renderer, keeping cover and body visually consistent."},
|
||||||
|
|
||||||
|
{"type":"h1", "text":"How It Works"},
|
||||||
|
{"type":"h2", "text":"The Token Pipeline"},
|
||||||
|
{"type":"body", "text":"The palette.py script infers a color palette and typography pair from the document type. These tokens are written to tokens.json and consumed by every downstream script."},
|
||||||
|
{"type":"numbered","text":"palette.py generates color tokens, font selection, and the cover pattern"},
|
||||||
|
{"type":"numbered","text":"cover.py renders the cover HTML using the selected pattern"},
|
||||||
|
{"type":"numbered","text":"render_cover.js uses Playwright to convert the HTML cover to PDF"},
|
||||||
|
{"type":"numbered","text":"render_body.py builds inner pages from content.json using ReportLab"},
|
||||||
|
{"type":"numbered","text":"merge.py combines cover + body and runs final QA checks"},
|
||||||
|
|
||||||
|
{"type":"h2", "text":"Cover Patterns"},
|
||||||
|
{"type":"table",
|
||||||
|
"headers": ["Pattern", "Document type", "Visual character"],
|
||||||
|
"rows": [
|
||||||
|
["fullbleed", "report, general", "Deep background · dot-grid texture"],
|
||||||
|
["split", "proposal", "Left dark panel · right dot-grid"],
|
||||||
|
["typographic", "resume, academic", "Oversized display type · first-word accent"],
|
||||||
|
["atmospheric", "portfolio", "Dark bg · radial glow · dot-grid"],
|
||||||
|
["magazine", "magazine", "Cream bg · centered · hero image"],
|
||||||
|
["darkroom", "darkroom", "Navy bg · centered · grayscale image"],
|
||||||
|
["terminal", "terminal", "Near-black · grid lines · monospace"],
|
||||||
|
["poster", "poster", "White · thick sidebar · oversized title"]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{"type":"h1", "text":"Data Visualisation"},
|
||||||
|
{"type":"h2", "text":"Performance Metrics (Chart)"},
|
||||||
|
{"type":"body", "text":"Charts are rendered natively using matplotlib with a color palette derived from the document accent. No external chart services or image files required."},
|
||||||
|
{"type":"chart",
|
||||||
|
"chart_type": "bar",
|
||||||
|
"title": "Quarterly Performance",
|
||||||
|
"labels": ["Q1", "Q2", "Q3", "Q4"],
|
||||||
|
"datasets": [
|
||||||
|
{"label": "Revenue", "values": [120, 145, 132, 178]},
|
||||||
|
{"label": "Expenses", "values": [95, 108, 99, 122]}
|
||||||
|
],
|
||||||
|
"y_label": "USD (thousands)",
|
||||||
|
"caption": "Quarterly revenue vs. expenses"
|
||||||
|
},
|
||||||
|
|
||||||
|
{"type":"h2", "text":"Market Share (Pie Chart)"},
|
||||||
|
{"type":"chart",
|
||||||
|
"chart_type": "pie",
|
||||||
|
"labels": ["Product A", "Product B", "Product C", "Other"],
|
||||||
|
"datasets": [{"values": [42, 28, 18, 12]}],
|
||||||
|
"caption": "Annual market share by product line"
|
||||||
|
},
|
||||||
|
|
||||||
|
{"type":"pagebreak"},
|
||||||
|
|
||||||
|
{"type":"h1", "text":"Mathematics"},
|
||||||
|
{"type":"body", "text":"Display math is rendered via matplotlib mathtext — no LaTeX binary installation required. Inline references use standard [N] notation in body text."},
|
||||||
|
{"type":"math", "text":"E = mc^2", "label":"(1)"},
|
||||||
|
{"type":"math", "text":"\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}", "label":"(2)"},
|
||||||
|
{"type":"math", "text":"\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}", "caption":"Basel problem (Euler, 1734)"},
|
||||||
|
|
||||||
|
{"type":"h1", "text":"Process Flow"},
|
||||||
|
{"type":"body", "text":"Flowcharts are drawn directly using matplotlib patches — no Graphviz or external tools needed. Supported node shapes: rect, diamond, oval, parallelogram."},
|
||||||
|
{"type":"flowchart",
|
||||||
|
"nodes": [
|
||||||
|
{"id":"start", "label":"Start", "shape":"oval"},
|
||||||
|
{"id":"input", "label":"Receive Input", "shape":"parallelogram"},
|
||||||
|
{"id":"valid", "label":"Valid?", "shape":"diamond"},
|
||||||
|
{"id":"proc", "label":"Process Data", "shape":"rect"},
|
||||||
|
{"id":"err", "label":"Return Error", "shape":"rect"},
|
||||||
|
{"id":"out", "label":"Return Result", "shape":"parallelogram"},
|
||||||
|
{"id":"end", "label":"End", "shape":"oval"}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{"from":"start", "to":"input"},
|
||||||
|
{"from":"input", "to":"valid"},
|
||||||
|
{"from":"valid", "to":"proc", "label":"Yes"},
|
||||||
|
{"from":"valid", "to":"err", "label":"No"},
|
||||||
|
{"from":"proc", "to":"out"},
|
||||||
|
{"from":"err", "to":"end"},
|
||||||
|
{"from":"out", "to":"end"}
|
||||||
|
],
|
||||||
|
"caption": "Data validation and processing flow"
|
||||||
|
},
|
||||||
|
|
||||||
|
{"type":"h1", "text":"Code Example"},
|
||||||
|
{"type":"code", "language":"python",
|
||||||
|
"text":"# Design token pipeline\ntokens = palette.build_tokens(\n title=\"Annual Report\",\n doc_type=\"report\",\n author=\"J. Smith\",\n date=\"March 2026\",\n)\nhtml = cover.render(tokens)\npdf = render_cover(html)"},
|
||||||
|
|
||||||
|
{"type":"h1", "text":"Design Principles"},
|
||||||
|
{"type":"body", "text":"The aesthetic system is documented in design/design.md. The core rule: every design decision must be rooted in the document content and purpose. A color chosen because it fits the content will always outperform a color chosen because it seems safe."},
|
||||||
|
{"type":"h2", "text":"Restraint over decoration"},
|
||||||
|
{"type":"body", "text":"The page is done when there is nothing left to remove. Accent color appears on section rules only — not on headings, not on bullets. No card components, no drop shadows."},
|
||||||
|
{"type":"callout", "text":"A PDF passes the quality bar when a designer would not be embarrassed to hand it to a client."},
|
||||||
|
|
||||||
|
{"type":"pagebreak"},
|
||||||
|
{"type":"bibliography",
|
||||||
|
"title": "References",
|
||||||
|
"items": [
|
||||||
|
{"id":"1","text":"Bringhurst, R. (2004). The Elements of Typographic Style (3rd ed.). Hartley & Marks."},
|
||||||
|
{"id":"2","text":"Cairo, A. (2016). The Truthful Art: Data, Charts, and Maps for Communication. New Riders."},
|
||||||
|
{"id":"3","text":"Hochuli, J. & Kinross, R. (1996). Designing Books: Practice and Theory. Hyphen Press."}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
JSON
|
||||||
|
|
||||||
|
cmd_run \
|
||||||
|
--title "minimax-pdf demo" \
|
||||||
|
--type "report" \
|
||||||
|
--author "minimax-pdf skill" \
|
||||||
|
--date "$(date '+%B %Y')" \
|
||||||
|
--subtitle "A demonstration of the token-based design pipeline" \
|
||||||
|
--content "$tmpdir/content.json" \
|
||||||
|
--out "demo.pdf"
|
||||||
|
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── dispatch ───────────────────────────────────────────────────────────────────
|
||||||
|
main() {
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
bold "minimax-pdf — make.sh"
|
||||||
|
echo ""
|
||||||
|
echo "Usage: bash make.sh <command> [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Commands:"
|
||||||
|
echo " check Verify all dependencies"
|
||||||
|
echo " fix Auto-install missing deps"
|
||||||
|
echo " run --title T --type TYPE CREATE: full pipeline → PDF"
|
||||||
|
echo " [--author A] [--date D] [--subtitle S]"
|
||||||
|
echo " [--abstract A] [--cover-image URL]"
|
||||||
|
echo " [--accent #HEX] [--cover-bg #HEX]"
|
||||||
|
echo " [--content content.json] [--out output.pdf]"
|
||||||
|
echo " fill --input f.pdf FILL: inspect or fill form fields"
|
||||||
|
echo " reformat --input doc.md REFORMAT: parse doc → apply design → PDF"
|
||||||
|
echo " demo Build a full-featured demo PDF"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
check) cmd_check ;;
|
||||||
|
fix) cmd_fix ;;
|
||||||
|
run) shift; cmd_run "$@" ;;
|
||||||
|
fill) shift; cmd_fill "$@" ;;
|
||||||
|
reformat) shift; cmd_reformat "$@" ;;
|
||||||
|
demo) cmd_demo ;;
|
||||||
|
*) echo "Unknown command: $1"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
112
skills/minimax-pdf/scripts/merge.py
Normal file
112
skills/minimax-pdf/scripts/merge.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
merge.py — Merge cover.pdf + body.pdf → final.pdf and print a QA report.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 merge.py --cover cover.pdf --body body.pdf --out final.pdf
|
||||||
|
python3 merge.py --cover cover.pdf --body body.pdf --out final.pdf --title "My Report"
|
||||||
|
|
||||||
|
Exit codes: 0 success, 1 bad args/missing file, 2 missing dep, 3 merge error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def ensure_deps():
|
||||||
|
if importlib.util.find_spec("pypdf") is None:
|
||||||
|
import subprocess
|
||||||
|
subprocess.check_call(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--break-system-packages", "-q", "pypdf"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ensure_deps()
|
||||||
|
|
||||||
|
from pypdf import PdfWriter, PdfReader
|
||||||
|
|
||||||
|
|
||||||
|
def merge(cover_path: str, body_path: str, out_path: str, title: str = "") -> dict:
|
||||||
|
writer = PdfWriter()
|
||||||
|
|
||||||
|
for fpath, label in [(cover_path, "cover"), (body_path, "body")]:
|
||||||
|
if not os.path.exists(fpath):
|
||||||
|
return {"status": "error", "error": f"{label} file not found: {fpath}"}
|
||||||
|
reader = PdfReader(fpath)
|
||||||
|
for page in reader.pages:
|
||||||
|
writer.add_page(page)
|
||||||
|
|
||||||
|
# Set PDF metadata
|
||||||
|
if title:
|
||||||
|
writer.add_metadata({"/Title": title})
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(os.path.abspath(out_path)), exist_ok=True)
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
writer.write(f)
|
||||||
|
|
||||||
|
size_kb = os.path.getsize(out_path) // 1024
|
||||||
|
total_pages = len(writer.pages)
|
||||||
|
|
||||||
|
# ── QA checks ─────────────────────────────────────────────────────────────
|
||||||
|
warnings = []
|
||||||
|
|
||||||
|
# Page count sanity
|
||||||
|
cover_pages = len(PdfReader(cover_path).pages)
|
||||||
|
body_pages = len(PdfReader(body_path).pages)
|
||||||
|
if cover_pages != 1:
|
||||||
|
warnings.append(f"Cover PDF has {cover_pages} pages (expected 1)")
|
||||||
|
|
||||||
|
# File size sanity
|
||||||
|
if size_kb < 20:
|
||||||
|
warnings.append(f"Output is very small ({size_kb} KB) — may have blank pages")
|
||||||
|
if size_kb > 50_000:
|
||||||
|
warnings.append(f"Output is very large ({size_kb} KB) — consider compressing images")
|
||||||
|
|
||||||
|
report = {
|
||||||
|
"status": "ok",
|
||||||
|
"out": out_path,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"cover_pages": cover_pages,
|
||||||
|
"body_pages": body_pages,
|
||||||
|
"size_kb": size_kb,
|
||||||
|
}
|
||||||
|
if warnings:
|
||||||
|
report["warnings"] = warnings
|
||||||
|
|
||||||
|
return report
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Merge cover + body PDFs")
|
||||||
|
parser.add_argument("--cover", required=True)
|
||||||
|
parser.add_argument("--body", required=True)
|
||||||
|
parser.add_argument("--out", required=True)
|
||||||
|
parser.add_argument("--title", default="")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
result = merge(args.cover, args.body, args.out, args.title)
|
||||||
|
|
||||||
|
if result["status"] == "error":
|
||||||
|
print(json.dumps(result), file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
print(json.dumps(result))
|
||||||
|
|
||||||
|
# Human-readable QA summary
|
||||||
|
print(f"\n── Build complete ──────────────────────────────────────")
|
||||||
|
print(f" Output : {result['out']}")
|
||||||
|
print(f" Pages : {result['total_pages']} total (1 cover + {result['body_pages']} body)")
|
||||||
|
print(f" Size : {result['size_kb']} KB")
|
||||||
|
if result.get("warnings"):
|
||||||
|
print(f" ⚠ Warnings:")
|
||||||
|
for w in result["warnings"]:
|
||||||
|
print(f" • {w}")
|
||||||
|
else:
|
||||||
|
print(f" ✓ No issues detected")
|
||||||
|
print(f"────────────────────────────────────────────────────────\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
521
skills/minimax-pdf/scripts/palette.py
Normal file
521
skills/minimax-pdf/scripts/palette.py
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
palette.py — Infer design tokens from document metadata.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 palette.py --title "AI Trends 2025" --type report --out tokens.json
|
||||||
|
python3 palette.py --title "John Doe Resume" --type resume --out tokens.json
|
||||||
|
python3 palette.py --meta meta.json --out tokens.json
|
||||||
|
|
||||||
|
Outputs tokens.json consumed by all downstream scripts.
|
||||||
|
Cover fonts are loaded via Google Fonts @import in the cover HTML (no local caching).
|
||||||
|
Body fonts always use ReportLab system fonts (Times-Bold / Helvetica).
|
||||||
|
Exit codes: 0 success, 1 bad args, 3 write error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# ── Palette library ────────────────────────────────────────────────────────────
|
||||||
|
# Each entry: cover colors + cover_pattern + mood
|
||||||
|
PALETTES = {
|
||||||
|
"report": {
|
||||||
|
# Charcoal blue-grey cover; muted steel blue accent — authoritative, not flashy
|
||||||
|
"cover_bg": "#1B2A38",
|
||||||
|
"accent": "#3B6D8A",
|
||||||
|
"accent_lt": "#E6EFF5",
|
||||||
|
"text_light": "#EDE9E2",
|
||||||
|
"page_bg": "#FAFAF8",
|
||||||
|
"dark": "#1A1E24",
|
||||||
|
"body_text": "#2C2C30",
|
||||||
|
"muted": "#7A7A84",
|
||||||
|
"cover_pattern": "fullbleed",
|
||||||
|
"mood": "authoritative",
|
||||||
|
},
|
||||||
|
"proposal": {
|
||||||
|
# Dark charcoal cover; slate grey-blue accent — confident, understated
|
||||||
|
"cover_bg": "#22272E",
|
||||||
|
"accent": "#4E6070",
|
||||||
|
"accent_lt": "#EAECEE",
|
||||||
|
"text_light": "#EDE9E2",
|
||||||
|
"page_bg": "#FAFAF7",
|
||||||
|
"dark": "#18191E",
|
||||||
|
"body_text": "#28282E",
|
||||||
|
"muted": "#7A7870",
|
||||||
|
"cover_pattern": "split",
|
||||||
|
"mood": "confident",
|
||||||
|
},
|
||||||
|
"resume": {
|
||||||
|
# White; deep navy accent — clean and unambiguous
|
||||||
|
"cover_bg": "#FFFFFF",
|
||||||
|
"accent": "#1C3557",
|
||||||
|
"accent_lt": "#E8EEF5",
|
||||||
|
"text_light": "#FFFFFF",
|
||||||
|
"page_bg": "#FFFFFF",
|
||||||
|
"dark": "#111111",
|
||||||
|
"body_text": "#222222",
|
||||||
|
"muted": "#888888",
|
||||||
|
"cover_pattern": "typographic",
|
||||||
|
"mood": "clean",
|
||||||
|
},
|
||||||
|
"portfolio": {
|
||||||
|
# Near-black charcoal; cool slate grey accent — subdued professional
|
||||||
|
"cover_bg": "#191C20",
|
||||||
|
"accent": "#6A7A88",
|
||||||
|
"accent_lt": "#EAECEE",
|
||||||
|
"text_light": "#EDE9E4",
|
||||||
|
"page_bg": "#F8F8F8",
|
||||||
|
"dark": "#18191E",
|
||||||
|
"body_text": "#28282E",
|
||||||
|
"muted": "#8A8A96",
|
||||||
|
"cover_pattern": "atmospheric",
|
||||||
|
"mood": "expressive",
|
||||||
|
},
|
||||||
|
"academic": {
|
||||||
|
# Warm white; classic navy accent — scholarly standard
|
||||||
|
"cover_bg": "#F5F4F0",
|
||||||
|
"accent": "#2A436A",
|
||||||
|
"accent_lt": "#E6EBF4",
|
||||||
|
"text_light": "#FFFFFF",
|
||||||
|
"page_bg": "#F5F4F0",
|
||||||
|
"dark": "#1A1A28",
|
||||||
|
"body_text": "#1E1E2A",
|
||||||
|
"muted": "#686877",
|
||||||
|
"cover_pattern": "typographic",
|
||||||
|
"mood": "scholarly",
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
# Dark slate; muted steel accent — neutral, no-nonsense
|
||||||
|
"cover_bg": "#1F2329",
|
||||||
|
"accent": "#4A6070",
|
||||||
|
"accent_lt": "#E6EAEC",
|
||||||
|
"text_light": "#EEEBE5",
|
||||||
|
"page_bg": "#F8F6F2",
|
||||||
|
"dark": "#1A1A1A",
|
||||||
|
"body_text": "#2C2C2C",
|
||||||
|
"muted": "#888888",
|
||||||
|
"cover_pattern": "fullbleed",
|
||||||
|
"mood": "neutral",
|
||||||
|
},
|
||||||
|
# ── Extended types — each uses a distinct new cover pattern ─────────────────
|
||||||
|
"minimal": {
|
||||||
|
# Warm off-white; dark neutral grey — truly restrained, no color signal
|
||||||
|
"cover_bg": "#F7F6F4",
|
||||||
|
"accent": "#4A4A4A",
|
||||||
|
"accent_lt": "#EBEBEA",
|
||||||
|
"text_light": "#F7F6F4",
|
||||||
|
"page_bg": "#F7F6F4",
|
||||||
|
"dark": "#111111",
|
||||||
|
"body_text": "#222222",
|
||||||
|
"muted": "#999999",
|
||||||
|
"cover_pattern": "minimal",
|
||||||
|
"mood": "restrained",
|
||||||
|
},
|
||||||
|
"stripe": {
|
||||||
|
# Near-black; charcoal slate accent — structured, no-nonsense
|
||||||
|
"cover_bg": "#1E222A",
|
||||||
|
"accent": "#4A5568",
|
||||||
|
"accent_lt": "#EAECEE",
|
||||||
|
"text_light": "#FFFFFF",
|
||||||
|
"page_bg": "#F8F8F7",
|
||||||
|
"dark": "#0E1117",
|
||||||
|
"body_text": "#262630",
|
||||||
|
"muted": "#888898",
|
||||||
|
"cover_pattern": "stripe",
|
||||||
|
"mood": "bold",
|
||||||
|
},
|
||||||
|
"diagonal": {
|
||||||
|
# Deep navy; muted slate-blue accent — dignified, controlled
|
||||||
|
"cover_bg": "#1A2535",
|
||||||
|
"accent": "#3D5A72",
|
||||||
|
"accent_lt": "#E4EBF0",
|
||||||
|
"text_light": "#EEF0F5",
|
||||||
|
"page_bg": "#F8FAFC",
|
||||||
|
"dark": "#0F1A2A",
|
||||||
|
"body_text": "#1E2C3A",
|
||||||
|
"muted": "#7A8A96",
|
||||||
|
"cover_pattern": "diagonal",
|
||||||
|
"mood": "dynamic",
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
# Warm parchment; dark muted brown — classical, formal
|
||||||
|
"cover_bg": "#F5F2EC",
|
||||||
|
"accent": "#5C4A38",
|
||||||
|
"accent_lt": "#EAE5DE",
|
||||||
|
"text_light": "#F5F2EC",
|
||||||
|
"page_bg": "#F5F2EC",
|
||||||
|
"dark": "#2A1E14",
|
||||||
|
"body_text": "#2C2018",
|
||||||
|
"muted": "#9A8A78",
|
||||||
|
"cover_pattern": "frame",
|
||||||
|
"mood": "classical",
|
||||||
|
},
|
||||||
|
"editorial": {
|
||||||
|
# White; deep burgundy accent — editorial weight without the shout
|
||||||
|
"cover_bg": "#FFFFFF",
|
||||||
|
"accent": "#7A2B36",
|
||||||
|
"accent_lt": "#EEE4E5",
|
||||||
|
"text_light": "#FFFFFF",
|
||||||
|
"page_bg": "#FFFFFF",
|
||||||
|
"dark": "#0A0A0A",
|
||||||
|
"body_text": "#1A1A1A",
|
||||||
|
"muted": "#777777",
|
||||||
|
"cover_pattern": "editorial",
|
||||||
|
"mood": "editorial",
|
||||||
|
},
|
||||||
|
# ── New patterns (v2) ────────────────────────────────────────────────────────
|
||||||
|
"magazine": {
|
||||||
|
# Warm linen; deep navy accent — formal publication standard
|
||||||
|
"cover_bg": "#F0EEE9",
|
||||||
|
"accent": "#1C3557",
|
||||||
|
"accent_lt": "#E4EBF3",
|
||||||
|
"text_light": "#FFFFFF",
|
||||||
|
"page_bg": "#F0EEE9",
|
||||||
|
"dark": "#0D1A2B",
|
||||||
|
"body_text": "#2A2A2A",
|
||||||
|
"muted": "#888888",
|
||||||
|
"cover_pattern": "magazine",
|
||||||
|
"mood": "magazine",
|
||||||
|
},
|
||||||
|
"darkroom": {
|
||||||
|
# Deep navy; muted steel-blue accent — premium, controlled
|
||||||
|
"cover_bg": "#151C27",
|
||||||
|
"accent": "#3D5A7A",
|
||||||
|
"accent_lt": "#E2EBF2",
|
||||||
|
"text_light": "#EDE9E2",
|
||||||
|
"page_bg": "#F7F7F5",
|
||||||
|
"dark": "#0A1018",
|
||||||
|
"body_text": "#2C2C2C",
|
||||||
|
"muted": "#8A9AB0",
|
||||||
|
"cover_pattern": "darkroom",
|
||||||
|
"mood": "darkroom",
|
||||||
|
},
|
||||||
|
"terminal": {
|
||||||
|
# Near-black; forest green accent — technical, serious (not neon)
|
||||||
|
"cover_bg": "#0D1117",
|
||||||
|
"accent": "#3D7A5C",
|
||||||
|
"accent_lt": "#E2EEE8",
|
||||||
|
"text_light": "#E6EDF3",
|
||||||
|
"page_bg": "#F8F8F6",
|
||||||
|
"dark": "#010409",
|
||||||
|
"body_text": "#2C2C2C",
|
||||||
|
"muted": "#5A7A6A",
|
||||||
|
"cover_pattern": "terminal",
|
||||||
|
"mood": "terminal",
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
# White; near-black accent sidebar — stark, unambiguous
|
||||||
|
"cover_bg": "#FFFFFF",
|
||||||
|
"accent": "#0A0A0A",
|
||||||
|
"accent_lt": "#EBEBEA",
|
||||||
|
"text_light": "#FFFFFF",
|
||||||
|
"page_bg": "#FFFFFF",
|
||||||
|
"dark": "#0A0A0A",
|
||||||
|
"body_text": "#1A1A1A",
|
||||||
|
"muted": "#888888",
|
||||||
|
"cover_pattern": "poster",
|
||||||
|
"mood": "poster",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Font pairs — CSS names for cover HTML, ReportLab names for body ─────────────
|
||||||
|
# cover uses Google Fonts via @import (no local disk caching needed)
|
||||||
|
# body always uses system fonts via ReportLab
|
||||||
|
FONT_PAIRS = {
|
||||||
|
"authoritative": {
|
||||||
|
"display_css": "Playfair Display",
|
||||||
|
"body_css": "IBM Plex Sans",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=IBM+Plex+Sans:ital,wght@0,400;0,600;1,400&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"confident": {
|
||||||
|
"display_css": "Syne",
|
||||||
|
"body_css": "Nunito Sans",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Syne:wght@600;800&family=Nunito+Sans:wght@400;600;700&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"clean": {
|
||||||
|
"display_css": "DM Serif Display",
|
||||||
|
"body_css": "DM Sans",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=DM+Sans:wght@300;400;500&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"expressive": {
|
||||||
|
"display_css": "Fraunces",
|
||||||
|
"body_css": "Inter",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Fraunces:ital,wght@0,700;0,900;1,900&family=Inter:wght@300;400;500&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"scholarly": {
|
||||||
|
"display_css": "EB Garamond",
|
||||||
|
"body_css": "Source Sans 3",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,700;1,400&family=Source+Sans+3:wght@400;600&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"display_css": "Outfit",
|
||||||
|
"body_css": "Outfit",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700;900&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"restrained": {
|
||||||
|
"display_css": "Cormorant Garamond",
|
||||||
|
"body_css": "Jost",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,600;1,300&family=Jost:wght@300;400;500&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"bold": {
|
||||||
|
"display_css": "Barlow Condensed",
|
||||||
|
"body_css": "Barlow",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@700;900&family=Barlow:wght@400;500;600&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"dynamic": {
|
||||||
|
"display_css": "Montserrat",
|
||||||
|
"body_css": "Montserrat",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,300;0,700;0,900;1,400&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"classical": {
|
||||||
|
"display_css": "Cormorant",
|
||||||
|
"body_css": "Crimson Pro",
|
||||||
|
"gfonts_import": "https://fonts.googleapis.com/css2?family=Cormorant:ital,wght@0,400;0,700;1,400&family=Crimson+Pro:wght@400;600&display=swap",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"editorial": {
|
||||||
|
"display_css": "Bebas Neue",
|
||||||
|
"body_css": "Libre Franklin",
|
||||||
|
"gfonts_import": (
|
||||||
|
"https://fonts.googleapis.com/css2?family=Bebas+Neue"
|
||||||
|
"&family=Libre+Franklin:ital,wght@0,400;0,700;1,400&display=swap"
|
||||||
|
),
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
# ── New moods (v2) ───────────────────────────────────────────────────────────
|
||||||
|
"magazine": {
|
||||||
|
"display_css": "Playfair Display",
|
||||||
|
"body_css": "EB Garamond",
|
||||||
|
"gfonts_import": (
|
||||||
|
"https://fonts.googleapis.com/css2?family=Playfair+Display"
|
||||||
|
":ital,wght@0,700;0,900;1,700"
|
||||||
|
"&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap"
|
||||||
|
),
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"darkroom": {
|
||||||
|
"display_css": "Playfair Display",
|
||||||
|
"body_css": "EB Garamond",
|
||||||
|
"gfonts_import": (
|
||||||
|
"https://fonts.googleapis.com/css2?family=Playfair+Display"
|
||||||
|
":ital,wght@0,700;0,900;1,700"
|
||||||
|
"&family=EB+Garamond:ital,wght@0,400;0,600;1,400&display=swap"
|
||||||
|
),
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
},
|
||||||
|
"terminal": {
|
||||||
|
"display_css": "Space Mono",
|
||||||
|
"body_css": "Space Mono",
|
||||||
|
"gfonts_import": (
|
||||||
|
"https://fonts.googleapis.com/css2?family=Space+Mono"
|
||||||
|
":ital,wght@0,400;0,700;1,400&display=swap"
|
||||||
|
),
|
||||||
|
"display_rl": "Courier-Bold",
|
||||||
|
"body_rl": "Courier",
|
||||||
|
"body_b_rl": "Courier-Bold",
|
||||||
|
},
|
||||||
|
"poster": {
|
||||||
|
"display_css": "Barlow Condensed",
|
||||||
|
"body_css": "Courier Prime",
|
||||||
|
"gfonts_import": (
|
||||||
|
"https://fonts.googleapis.com/css2?family=Barlow+Condensed"
|
||||||
|
":wght@700;900"
|
||||||
|
"&family=Courier+Prime:ital,wght@0,400;0,700;1,400&display=swap"
|
||||||
|
),
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Courier",
|
||||||
|
"body_b_rl": "Courier-Bold",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SYSTEM_FALLBACK = {
|
||||||
|
"display_css": "Georgia",
|
||||||
|
"body_css": "Arial",
|
||||||
|
"gfonts_import": "",
|
||||||
|
"display_rl": "Times-Bold",
|
||||||
|
"body_rl": "Helvetica",
|
||||||
|
"body_b_rl": "Helvetica-Bold",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Colour helpers ──────────────────────────────────────────────────────────────
|
||||||
|
def _hex_to_rgb(h: str) -> tuple:
|
||||||
|
h = h.lstrip("#")
|
||||||
|
return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
||||||
|
|
||||||
|
|
||||||
|
def _lighten(hex_color: str, factor: float = 0.09) -> str:
|
||||||
|
"""Blend hex_color toward white (factor = accent weight, 0=white, 1=full color)."""
|
||||||
|
r, g, b = _hex_to_rgb(hex_color)
|
||||||
|
return "#{:02X}{:02X}{:02X}".format(
|
||||||
|
round(r * factor + 255 * (1 - factor)),
|
||||||
|
round(g * factor + 255 * (1 - factor)),
|
||||||
|
round(b * factor + 255 * (1 - factor)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Token assembly ─────────────────────────────────────────────────────────────
|
||||||
|
def build_tokens(
|
||||||
|
title: str,
|
||||||
|
doc_type: str,
|
||||||
|
author: str = "",
|
||||||
|
date: str = "",
|
||||||
|
accent_override: str = "",
|
||||||
|
cover_bg_override: str = "",
|
||||||
|
) -> dict:
|
||||||
|
palette = PALETTES.get(doc_type, PALETTES["general"]).copy()
|
||||||
|
mood = palette["mood"]
|
||||||
|
font_pair = FONT_PAIRS.get(mood, SYSTEM_FALLBACK)
|
||||||
|
|
||||||
|
# Apply caller-supplied overrides before token assembly
|
||||||
|
if accent_override:
|
||||||
|
palette["accent"] = accent_override
|
||||||
|
palette["accent_lt"] = _lighten(accent_override, 0.09)
|
||||||
|
if cover_bg_override:
|
||||||
|
palette["cover_bg"] = cover_bg_override
|
||||||
|
|
||||||
|
tokens = {
|
||||||
|
# Identity
|
||||||
|
"title": title,
|
||||||
|
"author": author,
|
||||||
|
"date": date,
|
||||||
|
"doc_type": doc_type,
|
||||||
|
|
||||||
|
# Palette
|
||||||
|
"cover_bg": palette["cover_bg"],
|
||||||
|
"accent": palette["accent"],
|
||||||
|
"accent_lt": palette["accent_lt"],
|
||||||
|
"text_light": palette["text_light"],
|
||||||
|
"page_bg": palette["page_bg"],
|
||||||
|
"dark": palette["dark"],
|
||||||
|
"body_text": palette["body_text"],
|
||||||
|
"muted": palette["muted"],
|
||||||
|
"cover_pattern": palette["cover_pattern"],
|
||||||
|
"mood": mood,
|
||||||
|
|
||||||
|
# Typography — CSS names for cover HTML (loaded via Google Fonts @import)
|
||||||
|
"font_display": font_pair["display_css"],
|
||||||
|
"font_body": font_pair["body_css"],
|
||||||
|
"gfonts_import": font_pair["gfonts_import"],
|
||||||
|
|
||||||
|
# Typography — ReportLab system font names for body pages
|
||||||
|
"font_display_rl": font_pair["display_rl"],
|
||||||
|
"font_body_rl": font_pair["body_rl"],
|
||||||
|
"font_body_b_rl": font_pair["body_b_rl"],
|
||||||
|
|
||||||
|
# Legacy keys (kept so render_body.py's register_fonts is a no-op)
|
||||||
|
"font_heading": font_pair["display_rl"],
|
||||||
|
"font_body_b": font_pair["body_b_rl"],
|
||||||
|
"font_paths": {},
|
||||||
|
|
||||||
|
# Type scale (pt)
|
||||||
|
"size_display": 54,
|
||||||
|
"size_h1": 22,
|
||||||
|
"size_h2": 15,
|
||||||
|
"size_h3": 11.5,
|
||||||
|
"size_body": 10.5,
|
||||||
|
"size_caption": 8.5,
|
||||||
|
"size_meta": 8,
|
||||||
|
|
||||||
|
# Layout (pt, 1cm ≈ 28.35pt)
|
||||||
|
"margin_left": 79, # 2.8cm
|
||||||
|
"margin_right": 79,
|
||||||
|
"margin_top": 79,
|
||||||
|
"margin_bottom": 71, # 2.5cm
|
||||||
|
"section_gap": 26,
|
||||||
|
"para_gap": 8,
|
||||||
|
"line_gap": 17,
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI ───────────────────────────────────────────────────────────────────────
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Generate design tokens from document metadata")
|
||||||
|
parser.add_argument("--title", default="Untitled Document")
|
||||||
|
parser.add_argument("--type", default="general",
|
||||||
|
choices=list(PALETTES.keys()),
|
||||||
|
help="Document type: " + ", ".join(PALETTES.keys()))
|
||||||
|
parser.add_argument("--author", default="")
|
||||||
|
parser.add_argument("--date", default="")
|
||||||
|
parser.add_argument("--meta", help="JSON file with title/type/author/date keys")
|
||||||
|
parser.add_argument("--accent", default="",
|
||||||
|
help="Override accent colour (hex, e.g. #2D6A8F). "
|
||||||
|
"accent_lt is auto-derived by lightening toward white.")
|
||||||
|
parser.add_argument("--cover-bg", default="",
|
||||||
|
help="Override cover background colour (hex).")
|
||||||
|
parser.add_argument("--out", default="tokens.json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.meta:
|
||||||
|
try:
|
||||||
|
with open(args.meta) as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
args.title = meta.get("title", args.title)
|
||||||
|
args.type = meta.get("type", args.type)
|
||||||
|
args.author = meta.get("author", args.author)
|
||||||
|
args.date = meta.get("date", args.date)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
tokens = build_tokens(
|
||||||
|
args.title, args.type, args.author, args.date,
|
||||||
|
accent_override=args.accent,
|
||||||
|
cover_bg_override=getattr(args, "cover_bg", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(args.out, "w") as f:
|
||||||
|
json.dump(tokens, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"status": "error", "error": str(e)}), file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
print(json.dumps({
|
||||||
|
"status": "ok",
|
||||||
|
"out": args.out,
|
||||||
|
"mood": tokens["mood"],
|
||||||
|
"pattern": tokens["cover_pattern"],
|
||||||
|
"fonts": f'{tokens["font_display"]} / {tokens["font_body"]}',
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
374
skills/minimax-pdf/scripts/reformat_parse.py
Normal file
374
skills/minimax-pdf/scripts/reformat_parse.py
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
reformat_parse.py — Convert an existing document into content.json,
|
||||||
|
then hand off to the CREATE pipeline (render_body.py).
|
||||||
|
|
||||||
|
Supported input formats:
|
||||||
|
.md / .txt — Markdown / plain text
|
||||||
|
.pdf — Extract text from existing PDF (layout preserved as best-effort)
|
||||||
|
.json — Pass-through if already content.json format
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 reformat_parse.py --input doc.md --out content.json
|
||||||
|
python3 reformat_parse.py --input old.pdf --out content.json
|
||||||
|
python3 reformat_parse.py --input data.json --out content.json
|
||||||
|
|
||||||
|
Then pipe into the CREATE pipeline:
|
||||||
|
python3 render_body.py --tokens tokens.json --content content.json --out body.pdf
|
||||||
|
|
||||||
|
Or use make.sh reformat which does both steps:
|
||||||
|
bash make.sh reformat --input doc.md --type report --title "My Report" --out output.pdf
|
||||||
|
|
||||||
|
Exit codes: 0 success, 1 bad args / unsupported format, 2 dep missing, 3 parse error
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_deps():
|
||||||
|
missing = []
|
||||||
|
if importlib.util.find_spec("pypdf") is None:
|
||||||
|
missing.append("pypdf")
|
||||||
|
if missing:
|
||||||
|
import subprocess
|
||||||
|
subprocess.check_call(
|
||||||
|
[sys.executable, "-m", "pip", "install", "--break-system-packages", "-q"] + missing
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ensure_deps()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Markdown / plain text parser ───────────────────────────────────────────────
|
||||||
|
def parse_markdown(text: str) -> list:
|
||||||
|
"""
|
||||||
|
Convert Markdown to content.json blocks.
|
||||||
|
Supports: # headings, **bold**, bullet lists, > blockquotes (→ callout),
|
||||||
|
| tables |, plain paragraphs.
|
||||||
|
"""
|
||||||
|
blocks = []
|
||||||
|
lines = text.splitlines()
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
def flush_para(buf: list):
|
||||||
|
t = " ".join(buf).strip()
|
||||||
|
if t:
|
||||||
|
blocks.append({"type": "body", "text": _md_inline(t)})
|
||||||
|
|
||||||
|
para_buf = []
|
||||||
|
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
# Blank line — flush paragraph buffer
|
||||||
|
if not stripped:
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ATX Headings: # ## ###
|
||||||
|
m = re.match(r'^(#{1,3})\s+(.*)', stripped)
|
||||||
|
if m:
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
level = len(m.group(1))
|
||||||
|
htype = {1: "h1", 2: "h2", 3: "h3"}.get(level, "h3")
|
||||||
|
blocks.append({"type": htype, "text": _md_inline(m.group(2))})
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Display math block: $$expr$$ on one line, or opening $$ ... closing $$
|
||||||
|
if stripped.startswith("$$"):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
inline_expr = stripped[2:].rstrip("$").strip()
|
||||||
|
if inline_expr:
|
||||||
|
# Single-line: $$E = mc^2$$
|
||||||
|
blocks.append({"type": "math", "text": inline_expr})
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
# Multi-line: opening $$ alone, then expression lines, then closing $$
|
||||||
|
math_lines = []
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and lines[i].strip() != "$$":
|
||||||
|
math_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
if i < len(lines):
|
||||||
|
i += 1 # skip closing $$
|
||||||
|
blocks.append({"type": "math", "text": "\n".join(math_lines).strip()})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fenced code block: ``` or ~~~
|
||||||
|
if stripped.startswith("```") or stripped.startswith("~~~"):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
fence = stripped[:3]
|
||||||
|
code_lines = []
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and not lines[i].strip().startswith(fence):
|
||||||
|
code_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
if i < len(lines):
|
||||||
|
i += 1 # skip closing fence
|
||||||
|
blocks.append({"type": "code", "text": "\n".join(code_lines)})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Blockquote → callout
|
||||||
|
if stripped.startswith(">"):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
qt = re.sub(r'^>\s*', '', stripped)
|
||||||
|
blocks.append({"type": "callout", "text": _md_inline(qt)})
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Unordered bullet: -, *, +
|
||||||
|
if re.match(r'^[-*+]\s+', stripped):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
text_part = re.sub(r'^[-*+]\s+', '', stripped)
|
||||||
|
blocks.append({"type": "bullet", "text": _md_inline(text_part)})
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Ordered list: 1. 2. etc. → numbered (preserves counter in render_body)
|
||||||
|
if re.match(r'^\d+\.\s+', stripped):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
text_part = re.sub(r'^\d+\.\s+', '', stripped)
|
||||||
|
blocks.append({"type": "numbered", "text": _md_inline(text_part)})
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Table: | col | col |
|
||||||
|
if stripped.startswith("|"):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
table_lines = []
|
||||||
|
while i < len(lines) and lines[i].strip().startswith("|"):
|
||||||
|
table_lines.append(lines[i].strip())
|
||||||
|
i += 1
|
||||||
|
# Remove separator rows (|---|---|)
|
||||||
|
data_rows = [r for r in table_lines if not re.match(r'^\|[-:| ]+\|$', r)]
|
||||||
|
parsed = []
|
||||||
|
for row in data_rows:
|
||||||
|
cells = [c.strip() for c in row.strip("|").split("|")]
|
||||||
|
parsed.append(cells)
|
||||||
|
if len(parsed) >= 2:
|
||||||
|
blocks.append({
|
||||||
|
"type": "table",
|
||||||
|
"headers": parsed[0],
|
||||||
|
"rows": parsed[1:],
|
||||||
|
})
|
||||||
|
elif len(parsed) == 1:
|
||||||
|
# Single row — treat as paragraph
|
||||||
|
blocks.append({"type": "body", "text": " | ".join(parsed[0])})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Horizontal rule → spacer
|
||||||
|
if re.match(r'^[-*_]{3,}$', stripped):
|
||||||
|
flush_para(para_buf)
|
||||||
|
para_buf = []
|
||||||
|
blocks.append({"type": "spacer", "pt": 16})
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Plain text → accumulate into paragraph
|
||||||
|
para_buf.append(stripped)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
flush_para(para_buf)
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def _md_inline(text: str) -> str:
|
||||||
|
"""Convert inline Markdown to ReportLab XML markup."""
|
||||||
|
# Bold: **text** or __text__
|
||||||
|
text = re.sub(r'\*\*(.+?)\*\*', r'<b>\1</b>', text)
|
||||||
|
text = re.sub(r'__(.+?)__', r'<b>\1</b>', text)
|
||||||
|
# Italic: *text* or _text_
|
||||||
|
text = re.sub(r'\*(.+?)\*', r'<i>\1</i>', text)
|
||||||
|
text = re.sub(r'_(.+?)_', r'<i>\1</i>', text)
|
||||||
|
# Inline code: `code`
|
||||||
|
text = re.sub(r'`(.+?)`', r'<font name="Courier">\1</font>', text)
|
||||||
|
# Strip markdown links, keep text
|
||||||
|
text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# ── PDF text extractor ─────────────────────────────────────────────────────────
|
||||||
|
def parse_pdf(pdf_path: str) -> list:
|
||||||
|
"""
|
||||||
|
Extract text from an existing PDF and convert to content.json blocks.
|
||||||
|
Best-effort: detects headings by font size heuristics if available,
|
||||||
|
otherwise falls back to paragraph splitting.
|
||||||
|
"""
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
|
reader = PdfReader(pdf_path)
|
||||||
|
all_text = []
|
||||||
|
|
||||||
|
for page in reader.pages:
|
||||||
|
text = page.extract_text()
|
||||||
|
if text:
|
||||||
|
all_text.append(text.strip())
|
||||||
|
|
||||||
|
full_text = "\n\n".join(all_text)
|
||||||
|
|
||||||
|
# Treat extracted PDF text as plain text / light markdown
|
||||||
|
# (most PDFs lose formatting — we do our best)
|
||||||
|
return parse_plain(full_text)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_plain(text: str) -> list:
|
||||||
|
"""
|
||||||
|
Heuristic plain-text parser.
|
||||||
|
Short ALL-CAPS or title-case lines → headings.
|
||||||
|
Everything else → paragraphs.
|
||||||
|
"""
|
||||||
|
blocks = []
|
||||||
|
paragraphs = re.split(r'\n{2,}', text.strip())
|
||||||
|
|
||||||
|
for para in paragraphs:
|
||||||
|
para = para.strip()
|
||||||
|
if not para:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lines = para.splitlines()
|
||||||
|
|
||||||
|
# Single short line that looks like a heading
|
||||||
|
if len(lines) == 1 and len(para) < 80:
|
||||||
|
if para.isupper() or re.match(r'^[A-Z][^.!?]*$', para):
|
||||||
|
blocks.append({"type": "h1", "text": para.title()})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Bullet lists
|
||||||
|
if lines[0].startswith(("- ", "• ", "* ")):
|
||||||
|
for line in lines:
|
||||||
|
text_part = re.sub(r'^[-•*]\s+', '', line.strip())
|
||||||
|
if text_part:
|
||||||
|
blocks.append({"type": "bullet", "text": text_part})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Regular paragraph
|
||||||
|
blocks.append({"type": "body", "text": " ".join(lines)})
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
# ── Pass-through validator ─────────────────────────────────────────────────────
|
||||||
|
VALID_TYPES = {"h1","h2","h3","body","bullet","numbered","callout","table",
|
||||||
|
"image","code","math","divider","caption","pagebreak","spacer"}
|
||||||
|
|
||||||
|
def validate_content_json(data: list) -> tuple[list, list]:
|
||||||
|
"""Return (valid_blocks, warnings)."""
|
||||||
|
valid, warnings = [], []
|
||||||
|
for i, block in enumerate(data):
|
||||||
|
if not isinstance(block, dict):
|
||||||
|
warnings.append(f"Block {i}: not a dict, skipped")
|
||||||
|
continue
|
||||||
|
btype = block.get("type")
|
||||||
|
if btype not in VALID_TYPES:
|
||||||
|
warnings.append(f"Block {i}: unknown type '{btype}', kept as-is")
|
||||||
|
valid.append(block)
|
||||||
|
return valid, warnings
|
||||||
|
|
||||||
|
|
||||||
|
# ── Dispatcher ─────────────────────────────────────────────────────────────────
|
||||||
|
def parse_file(input_path: str) -> tuple[list, list]:
|
||||||
|
"""Return (blocks, warnings)."""
|
||||||
|
ext = Path(input_path).suffix.lower()
|
||||||
|
|
||||||
|
if ext in (".md", ".txt", ".markdown"):
|
||||||
|
with open(input_path, encoding="utf-8", errors="replace") as f:
|
||||||
|
text = f.read()
|
||||||
|
blocks = parse_markdown(text)
|
||||||
|
return blocks, []
|
||||||
|
|
||||||
|
if ext == ".pdf":
|
||||||
|
blocks = parse_pdf(input_path)
|
||||||
|
return blocks, ["PDF text extraction is best-effort — review content.json before rendering"]
|
||||||
|
|
||||||
|
if ext == ".json":
|
||||||
|
with open(input_path) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if isinstance(data, list):
|
||||||
|
return validate_content_json(data)
|
||||||
|
# Maybe it's a meta-wrapper {"content": [...]}
|
||||||
|
if isinstance(data, dict) and "content" in data:
|
||||||
|
return validate_content_json(data["content"])
|
||||||
|
return [], [f"JSON file does not contain a list of content blocks"]
|
||||||
|
|
||||||
|
return [], [f"Unsupported file type: {ext}. Supported: .md .txt .pdf .json"]
|
||||||
|
|
||||||
|
|
||||||
|
# ── CLI ────────────────────────────────────────────────────────────────────────
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Parse a document into content.json")
|
||||||
|
parser.add_argument("--input", required=True, help="Input file (.md, .txt, .pdf, .json)")
|
||||||
|
parser.add_argument("--out", default="content.json", help="Output content.json path")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
print(json.dumps({"status": "error", "error": f"File not found: {args.input}"}),
|
||||||
|
file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
blocks, warnings = parse_file(args.input)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(json.dumps({"status": "error", "error": str(e),
|
||||||
|
"trace": traceback.format_exc()}), file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
if not blocks:
|
||||||
|
print(json.dumps({
|
||||||
|
"status": "error",
|
||||||
|
"error": "No content blocks extracted",
|
||||||
|
"warnings": warnings,
|
||||||
|
}), file=sys.stderr)
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
with open(args.out, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(blocks, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "ok",
|
||||||
|
"out": args.out,
|
||||||
|
"block_count": len(blocks),
|
||||||
|
"warnings": warnings,
|
||||||
|
}
|
||||||
|
print(json.dumps(result, indent=2))
|
||||||
|
|
||||||
|
print(f"\n── Parsed {args.input} ─────────────────────────────────────",
|
||||||
|
file=sys.stderr)
|
||||||
|
print(f" Blocks : {len(blocks)}", file=sys.stderr)
|
||||||
|
|
||||||
|
type_counts: dict = {}
|
||||||
|
for b in blocks:
|
||||||
|
type_counts[b.get("type","?")] = type_counts.get(b.get("type","?"), 0) + 1
|
||||||
|
for t, n in sorted(type_counts.items()):
|
||||||
|
print(f" {t:12} × {n}", file=sys.stderr)
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
print(f" Warnings:", file=sys.stderr)
|
||||||
|
for w in warnings:
|
||||||
|
print(f" ⚠ {w}", file=sys.stderr)
|
||||||
|
print(f"\n Next: bash make.sh run --content {args.out} --title '...' --type ...",
|
||||||
|
file=sys.stderr)
|
||||||
|
print("", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1052
skills/minimax-pdf/scripts/render_body.py
Normal file
1052
skills/minimax-pdf/scripts/render_body.py
Normal file
File diff suppressed because it is too large
Load Diff
111
skills/minimax-pdf/scripts/render_cover.js
Normal file
111
skills/minimax-pdf/scripts/render_cover.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* render_cover.js — Render cover.html → cover.pdf via Playwright.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node render_cover.js --input cover.html --out cover.pdf
|
||||||
|
* node render_cover.js --input cover.html --out cover.pdf --wait 1200
|
||||||
|
*
|
||||||
|
* Exit codes: 0 success, 1 bad args, 2 dependency missing, 3 render error
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
function usage() {
|
||||||
|
console.error("Usage: node render_cover.js --input <file.html> --out <file.pdf> [--wait <ms>]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Arg parsing ────────────────────────────────────────────────────────────────
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
let inputFile = null, outFile = null, waitMs = 800;
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === "--input" && args[i + 1]) { inputFile = args[++i]; }
|
||||||
|
else if (args[i] === "--out" && args[i + 1]) { outFile = args[++i]; }
|
||||||
|
else if (args[i] === "--wait" && args[i + 1]) { waitMs = parseInt(args[++i], 10); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputFile || !outFile) usage();
|
||||||
|
if (!fs.existsSync(inputFile)) {
|
||||||
|
console.error(JSON.stringify({ status: "error", error: `File not found: ${inputFile}` }));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Playwright loader (tolerates global npm installs) ─────────────────────────
|
||||||
|
function loadPlaywright() {
|
||||||
|
const { execSync } = require("child_process");
|
||||||
|
try { return require("playwright"); } catch (_) {}
|
||||||
|
try {
|
||||||
|
const root = execSync("npm root -g", { stdio: ["ignore","pipe","ignore"] }).toString().trim();
|
||||||
|
return require(path.join(root, "playwright"));
|
||||||
|
} catch (_) {}
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
status: "error",
|
||||||
|
error: "playwright not found",
|
||||||
|
hint: "Run: npm install -g playwright && npx playwright install chromium"
|
||||||
|
}));
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main ───────────────────────────────────────────────────────────────────────
|
||||||
|
(async () => {
|
||||||
|
const { chromium } = loadPlaywright();
|
||||||
|
|
||||||
|
let browser;
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch();
|
||||||
|
} catch (e) {
|
||||||
|
// Chromium binary missing — try installing
|
||||||
|
const { spawnSync } = require("child_process");
|
||||||
|
const r = spawnSync("npx", ["playwright", "install", "chromium"], { stdio: "inherit", shell: true });
|
||||||
|
if (r.status !== 0) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
status: "error",
|
||||||
|
error: "Chromium not installed and auto-install failed",
|
||||||
|
hint: "Run: npx playwright install chromium"
|
||||||
|
}));
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
browser = await chromium.launch();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const page = await browser.newPage();
|
||||||
|
const fileUrl = "file://" + path.resolve(inputFile);
|
||||||
|
await page.goto(fileUrl);
|
||||||
|
await page.waitForTimeout(waitMs); // let CSS + any JS settle
|
||||||
|
|
||||||
|
await page.pdf({
|
||||||
|
path: outFile,
|
||||||
|
width: "794px",
|
||||||
|
height: "1123px",
|
||||||
|
printBackground: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Basic sanity: output file must exist and be > 5 KB
|
||||||
|
const stat = fs.statSync(outFile);
|
||||||
|
if (stat.size < 5000) {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
status: "error",
|
||||||
|
error: "Output PDF is suspiciously small — cover may be blank",
|
||||||
|
hint: "Check cover.html for render errors"
|
||||||
|
}));
|
||||||
|
process.exit(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
status: "ok",
|
||||||
|
out: outFile,
|
||||||
|
size_kb: Math.round(stat.size / 1024),
|
||||||
|
}));
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
if (browser) await browser.close().catch(() => {});
|
||||||
|
console.error(JSON.stringify({ status: "error", error: String(e) }));
|
||||||
|
process.exit(3);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
---
|
---
|
||||||
name: pptx-generator
|
name: pptx-generator
|
||||||
description: "Generate, edit, and read PowerPoint presentations. Create from scratch with PptxGenJS (cover, TOC, content, section divider, summary slides), edit existing PPTX via XML workflows, or extract text with markitdown. Triggers: PPT, PPTX, PowerPoint, presentation, slide, 幻灯片, 演示文稿."
|
description: "Generate, edit, and read PowerPoint presentations. Create from scratch with PptxGenJS (cover, TOC, content, section divider, summary slides), edit existing PPTX via XML workflows, or extract text with markitdown. Triggers: PPT, PPTX, PowerPoint, presentation, slide, deck, slides."
|
||||||
|
license: MIT
|
||||||
|
metadata:
|
||||||
|
version: "1.0"
|
||||||
|
category: productivity
|
||||||
|
sources:
|
||||||
|
- https://gitbrent.github.io/PptxGenJS/
|
||||||
|
- https://github.com/microsoft/markitdown
|
||||||
---
|
---
|
||||||
|
|
||||||
# PPTX Generator & Editor
|
# PPTX Generator & Editor
|
||||||
@@ -14,7 +21,7 @@ This skill handles all PowerPoint tasks: reading/analyzing existing presentation
|
|||||||
| Task | Approach |
|
| Task | Approach |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| Read/analyze content | `python -m markitdown presentation.pptx` |
|
| Read/analyze content | `python -m markitdown presentation.pptx` |
|
||||||
| Edit or create from template | See [Editing Presentations](editing.md) |
|
| Edit or create from template | See [Editing Presentations](references/editing.md) |
|
||||||
| Create from scratch | See [Creating from Scratch](#creating-from-scratch-workflow) below |
|
| Create from scratch | See [Creating from Scratch](#creating-from-scratch-workflow) below |
|
||||||
|
|
||||||
| Item | Value |
|
| Item | Value |
|
||||||
@@ -32,11 +39,11 @@ This skill handles all PowerPoint tasks: reading/analyzing existing presentation
|
|||||||
|
|
||||||
| File | Contents |
|
| File | Contents |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| [slide-types.md](slide-types.md) | 5 slide page types (Cover, TOC, Section Divider, Content, Summary) + additional layout patterns |
|
| [slide-types.md](references/slide-types.md) | 5 slide page types (Cover, TOC, Section Divider, Content, Summary) + additional layout patterns |
|
||||||
| [design-system.md](design-system.md) | Color palettes, font reference, style recipes (Sharp/Soft/Rounded/Pill), typography & spacing |
|
| [design-system.md](references/design-system.md) | Color palettes, font reference, style recipes (Sharp/Soft/Rounded/Pill), typography & spacing |
|
||||||
| [editing.md](editing.md) | Template-based editing workflow, XML manipulation, formatting rules, common pitfalls |
|
| [editing.md](references/editing.md) | Template-based editing workflow, XML manipulation, formatting rules, common pitfalls |
|
||||||
| [pitfalls.md](pitfalls.md) | QA process, common mistakes, critical PptxGenJS pitfalls |
|
| [pitfalls.md](references/pitfalls.md) | QA process, common mistakes, critical PptxGenJS pitfalls |
|
||||||
| [pptxgenjs.md](pptxgenjs.md) | Complete PptxGenJS API reference |
|
| [pptxgenjs.md](references/pptxgenjs.md) | Complete PptxGenJS API reference |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -59,19 +66,19 @@ Search to understand user requirements — topic, audience, purpose, tone, conte
|
|||||||
|
|
||||||
### Step 2: Select Color Palette & Fonts
|
### Step 2: Select Color Palette & Fonts
|
||||||
|
|
||||||
Use the [Color Palette Reference](design-system.md#color-palette-reference) to select a palette matching the topic and audience. Use the [Font Reference](design-system.md#font-reference) to choose a font pairing.
|
Use the [Color Palette Reference](references/design-system.md#color-palette-reference) to select a palette matching the topic and audience. Use the [Font Reference](references/design-system.md#font-reference) to choose a font pairing.
|
||||||
|
|
||||||
### Step 3: Select Design Style
|
### Step 3: Select Design Style
|
||||||
|
|
||||||
Use the [Style Recipes](design-system.md#style-recipes--视觉风格系统) to choose a visual style (Sharp, Soft, Rounded, or Pill) matching the presentation tone.
|
Use the [Style Recipes](references/design-system.md#style-recipes) to choose a visual style (Sharp, Soft, Rounded, or Pill) matching the presentation tone.
|
||||||
|
|
||||||
### Step 4: Plan Slide Outline
|
### Step 4: Plan Slide Outline
|
||||||
|
|
||||||
Classify **every slide** as exactly one of the [5 page types](slide-types.md). Plan the content and layout for each slide. Ensure visual variety — do NOT repeat the same layout across slides.
|
Classify **every slide** as exactly one of the [5 page types](references/slide-types.md). Plan the content and layout for each slide. Ensure visual variety — do NOT repeat the same layout across slides.
|
||||||
|
|
||||||
### Step 5: Generate Slide JS Files
|
### Step 5: Generate Slide JS Files
|
||||||
|
|
||||||
Create one JS file per slide in `slides/` directory. Each file must export a synchronous `createSlide(pres, theme)` function. Follow the [Slide Output Format](#slide-output-format) and the type-specific guidance in [slide-types.md](slide-types.md). Generate up to 5 slides concurrently using subagents if available.
|
Create one JS file per slide in `slides/` directory. Each file must export a synchronous `createSlide(pres, theme)` function. Follow the [Slide Output Format](#slide-output-format) and the type-specific guidance in [slide-types.md](references/slide-types.md). Generate up to 5 slides concurrently using subagents if available.
|
||||||
|
|
||||||
**Tell each subagent:**
|
**Tell each subagent:**
|
||||||
1. File naming: `slides/slide-01.js`, `slides/slide-02.js`, etc.
|
1. File naming: `slides/slide-01.js`, `slides/slide-02.js`, etc.
|
||||||
@@ -81,7 +88,7 @@ Create one JS file per slide in `slides/` directory. Each file must export a syn
|
|||||||
5. Fonts: Chinese = Microsoft YaHei, English = Arial (or approved alternative)
|
5. Fonts: Chinese = Microsoft YaHei, English = Arial (or approved alternative)
|
||||||
6. Colors: 6-char hex without # (e.g. `"FF0000"`)
|
6. Colors: 6-char hex without # (e.g. `"FF0000"`)
|
||||||
7. Must use the theme object contract (see [Theme Object Contract](#theme-object-contract))
|
7. Must use the theme object contract (see [Theme Object Contract](#theme-object-contract))
|
||||||
8. Must follow the [PptxGenJS API reference](pptxgenjs.md)
|
8. Must follow the [PptxGenJS API reference](references/pptxgenjs.md)
|
||||||
|
|
||||||
### Step 6: Compile into Final PPTX
|
### Step 6: Compile into Final PPTX
|
||||||
|
|
||||||
@@ -114,7 +121,7 @@ Run with: `cd slides && node compile.js`
|
|||||||
|
|
||||||
### Step 7: QA (Required)
|
### Step 7: QA (Required)
|
||||||
|
|
||||||
See [QA Process](pitfalls.md#qa-process).
|
See [QA Process](references/pitfalls.md#qa-process).
|
||||||
|
|
||||||
### Output Structure
|
### Output Structure
|
||||||
|
|
||||||
|
|||||||
@@ -1,392 +0,0 @@
|
|||||||
# Design System
|
|
||||||
|
|
||||||
## Color Palette Reference
|
|
||||||
|
|
||||||
| # | 名称 | 色值 | 风格 | 适用场景 | 建议 |
|
|
||||||
|---|------|------|------|----------|------|
|
|
||||||
| 1 | 现代与健康 | `#006d77` `#83c5be` `#edf6f9` `#ffddd2` `#e29578` | 清新、治愈 | 医疗健康、心理咨询、护肤品、瑜伽Spa | 深青做标题,浅粉做背景 |
|
|
||||||
| 2 | 商务与权威 | `#2b2d42` `#8d99ae` `#edf2f4` `#ef233c` `#d90429` | 严谨、经典 | 年度汇报、金融分析、企业介绍、政务报告 | 深蓝显专业,亮红强调数据 |
|
|
||||||
| 3 | 自然与户外 | `#606c38` `#283618` `#fefae0` `#dda15e` `#bc6c25` | 沉稳、大地色 | 户外用品、环境保护、农业项目、历史文化 | 深绿为底,米色为字 |
|
|
||||||
| 4 | 复古与学院 | `#780000` `#c1121f` `#fdf0d5` `#003049` `#669bbc` | 经典、书卷气 | 学术讲座、历史回顾、博物馆、复古品牌 | 深红与深蓝对比强烈 |
|
|
||||||
| 5 | 柔美与创意 | `#cdb4db` `#ffc8dd` `#ffafcc` `#bde0fe` `#a2d2ff` | 梦幻、糖果色 | 母婴产品、甜品店、女性时尚、幼儿园 | 文字用深灰或黑色 |
|
|
||||||
| 6 | 波西米亚 | `#ccd5ae` `#e9edc9` `#fefae0` `#faedcd` `#d4a373` | 温柔、低饱和 | 婚礼策划、家居软装、有机食品、慢生活 | 米色背景,绿棕点缀 |
|
|
||||||
| 7 | 活力与科技 | `#8ecae6` `#219ebc` `#023047` `#ffb703` `#fb8500` | 高能量、运动 | 体育赛事、健身房、创业路演、少儿教育 | 深蓝稳重心,橙色做焦点 |
|
|
||||||
| 8 | 匠心与手作 | `#7f5539` `#a68a64` `#ede0d4` `#656d4a` `#414833` | 质朴、咖啡调 | 咖啡店、手工艺品、传统文化、烘焙教学 | 适合纸质/皮革质感 |
|
|
||||||
| 9 | 科技与夜景 | `#000814` `#001d3d` `#003566` `#ffc300` `#ffd60a` | 深邃、高亮 | 科技发布会、星空天文、夜间经济、高端汽车 | 必须用深色模式 |
|
|
||||||
| 10 | 教育与图表 | `#264653` `#2a9d8f` `#e9c46a` `#f4a261` `#e76f51` | 清晰、逻辑强 | 统计报告、教育培训、市场分析、通用商务 | 完美的图表配色 |
|
|
||||||
| 11 | 森林与环保 | `#dad7cd` `#a3b18a` `#588157` `#3a5a40` `#344e41` | 单色渐变、森系 | 园林设计、ESG报告、环保公益、植物研究 | 单色系安全不会乱 |
|
|
||||||
| 12 | 优雅与时尚 | `#edafb8` `#f7e1d7` `#dedbd2` `#b0c4b1` `#4a5759` | 低饱和、莫兰迪 | 高定服装、艺术画廊、美妆品牌、杂志风 | 留白是关键 |
|
|
||||||
| 13 | 艺术与美食 | `#335c67` `#fff3b0` `#e09f3e` `#9e2a2b` `#540b0e` | 浓郁、复古画报 | 美食纪录片、艺术展、民族风情、复古餐厅 | 适合大色块拼接 |
|
|
||||||
| 14 | 轻奢与神秘 | `#22223b` `#4a4e69` `#9a8c98` `#c9ada7` `#f2e9e4` | 冷艳、紫调 | 珠宝展示、酒店管理、高端咨询、心理学 | 紫色营造高端氛围 |
|
|
||||||
| 15 | 纯净科技蓝 | `#03045e` `#0077b6` `#00b4d8` `#90e0ef` `#caf0f8` | 未来感、纯净 | 云计算/AI、水利海洋、医院医疗、洁净能源 | 从深海到天空的渐变 |
|
|
||||||
| 16 | 海岸珊瑚 | `#0081a7` `#00afb9` `#fdfcdc` `#fed9b7` `#f07167` | 清爽、夏日感 | 旅游度假、夏季活动、饮品品牌、海洋主题 | 青色与珊瑚色互补亮眼 |
|
|
||||||
| 17 | 活力橙薄荷 | `#ff9f1c` `#ffbf69` `#ffffff` `#cbf3f0` `#2ec4b6` | 明亮、欢快 | 儿童活动、促销海报、快消品、社交媒体 | 橙色吸睛,薄荷绿清爽 |
|
|
||||||
| 18 | 铂金白金 | `#0a0a0a` `#0070F3` `#D4AF37` `#f5f5f5` `#ffffff` | 高端、专业 | Agent产品、企业官网、金融科技、高端品牌 | 白金主调,蓝色行动,金色强调 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Agent Design System — 完整色板
|
|
||||||
|
|
||||||
基于 tokens.css/ts 的 Platinum White-Gold Theme,提供完整色阶供精细设计使用。
|
|
||||||
|
|
||||||
#### White 白色系(背景与浅色表面)
|
|
||||||
|
|
||||||
| 色阶 | 色值 | 用途 |
|
|
||||||
|------|------|------|
|
|
||||||
| white-0 | `#ffffff` | 主背景 |
|
|
||||||
| white-50 | `#fefefe` | 略带暖调的白 |
|
|
||||||
| white-75 | `#fcfcfc` | 微灰白 |
|
|
||||||
| white-100 | `#fafafa` | 次级背景 |
|
|
||||||
| white-200 | `#f7f7f7` | 卡片背景 |
|
|
||||||
| white-300 | `#f5f5f5` | 三级背景 |
|
|
||||||
| white-400 | `#f0f0f0` | 分隔区域 |
|
|
||||||
| white-500 | `#ebebeb` | 边框浅色 |
|
|
||||||
| white-600 | `#e5e5e5` | 禁用态背景 |
|
|
||||||
| white-700 | `#e0e0e0` | 深灰白 |
|
|
||||||
| white-800 | `#d9d9d9` | 占位符 |
|
|
||||||
| white-900 | `#d4d4d4` | 分隔线 |
|
|
||||||
| white-1000 | `#cccccc` | 最深白 |
|
|
||||||
|
|
||||||
#### Gold 金色系(铂金商务强调色)
|
|
||||||
|
|
||||||
| 色阶 | 色值 | 用途 |
|
|
||||||
|------|------|------|
|
|
||||||
| gold-25 | `#FFFDF5` | 极浅金背景 |
|
|
||||||
| gold-50 | `#FEF9E7` | 浅金背景 |
|
|
||||||
| gold-75 | `#FCF3D0` | 淡金高亮 |
|
|
||||||
| gold-100 | `#FAECB8` | 金色 hover 态 |
|
|
||||||
| gold-200 | `#F5DC8A` | 亮金强调 |
|
|
||||||
| gold-300 | `#E8C860` | 金色悬停 |
|
|
||||||
| gold-400 | `#D4AF37` | **主金色(核心)** |
|
|
||||||
| gold-500 | `#B8972E` | 金色文字 |
|
|
||||||
| gold-600 | `#9A7E26` | 深金强调 |
|
|
||||||
| gold-700 | `#7C651E` | 暗金边框 |
|
|
||||||
| gold-800 | `#5E4C16` | 深金背景 |
|
|
||||||
| gold-900 | `#40330F` | 极深金 |
|
|
||||||
| gold-1000 | `#221A08` | 黑金 |
|
|
||||||
|
|
||||||
#### Blue 蓝色系(主操作色)
|
|
||||||
|
|
||||||
| 色阶 | 色值 | 用途 |
|
|
||||||
|------|------|------|
|
|
||||||
| blue-25 | `#F0F7FF` | 极浅蓝背景 |
|
|
||||||
| blue-50 | `#E0EFFF` | 信息提示背景 |
|
|
||||||
| blue-75 | `#C2DFFF` | 浅蓝高亮 |
|
|
||||||
| blue-100 | `#A3CFFF` | 禁用态蓝 |
|
|
||||||
| blue-200 | `#66AFFF` | 亮蓝 |
|
|
||||||
| blue-300 | `#338FFF` | 蓝色悬停 |
|
|
||||||
| blue-400 | `#0070F3` | **主蓝色(核心)** |
|
|
||||||
| blue-500 | `#005FCC` | 蓝色文字 |
|
|
||||||
| blue-600 | `#004FA6` | 深蓝强调 |
|
|
||||||
| blue-700 | `#003F80` | 暗蓝边框 |
|
|
||||||
| blue-800 | `#002F5A` | 深蓝背景 |
|
|
||||||
| blue-900 | `#001F3D` | 极深蓝 |
|
|
||||||
| blue-1000 | `#001026` | 黑蓝 |
|
|
||||||
|
|
||||||
#### Gray 灰色系(文字与中性色)
|
|
||||||
|
|
||||||
| 色阶 | 色值 | 用途 |
|
|
||||||
|------|------|------|
|
|
||||||
| gray-0 | `#ffffff` | 白色 |
|
|
||||||
| gray-50 | `#fafafa` | 极浅灰 |
|
|
||||||
| gray-75 | `#f5f5f5` | 浅灰背景 |
|
|
||||||
| gray-100 | `#ededed` | 分隔线浅 |
|
|
||||||
| gray-200 | `#d4d4d4` | 边框浅 |
|
|
||||||
| gray-300 | `#a3a3a3` | 四级文字 |
|
|
||||||
| gray-400 | `#737373` | 三级文字 |
|
|
||||||
| gray-500 | `#525252` | 二级文字 |
|
|
||||||
| gray-600 | `#404040` | 深灰 |
|
|
||||||
| gray-700 | `#2e2e2e` | 暗色背景 |
|
|
||||||
| gray-800 | `#1f1f1f` | 深色背景 |
|
|
||||||
| gray-900 | `#141414` | 极深背景 |
|
|
||||||
| gray-1000 | `#0a0a0a` | **主文字色(核心)** |
|
|
||||||
|
|
||||||
#### 透明度色值
|
|
||||||
|
|
||||||
##### Opacity Black(黑色透明)
|
|
||||||
|
|
||||||
| 透明度 | 色值 | 用途 |
|
|
||||||
|--------|------|------|
|
|
||||||
| 0% | `#0a0a0a00` | 全透明 |
|
|
||||||
| 2% | `#0a0a0a05` | 微弱遮罩 |
|
|
||||||
| 4% | `#0a0a0a0a` | 次级交互背景 |
|
|
||||||
| 8% | `#0a0a0a14` | 边框/分隔 |
|
|
||||||
| 15% | `#0a0a0a26` | 按压态 |
|
|
||||||
| 20% | `#0a0a0a33` | 浅遮罩 |
|
|
||||||
| 25% | `#0a0a0a40` | 中遮罩 |
|
|
||||||
| 50% | `#0a0a0a80` | 半透明 |
|
|
||||||
| 70% | `#0a0a0ab2` | 深遮罩 |
|
|
||||||
| 80% | `#0a0a0acc` | 悬停态 |
|
|
||||||
| 90% | `#0a0a0ae5` | tooltip |
|
|
||||||
| 95% | `#0a0a0af2` | 弹窗 |
|
|
||||||
|
|
||||||
##### Opacity White(白色透明)
|
|
||||||
|
|
||||||
| 透明度 | 色值 | 用途 |
|
|
||||||
|--------|------|------|
|
|
||||||
| 0% | `#ffffff00` | 全透明 |
|
|
||||||
| 2% | `#ffffff05` | 微弱遮罩 |
|
|
||||||
| 4% | `#ffffff0a` | 次级交互背景 |
|
|
||||||
| 8% | `#ffffff12` | 边框/分隔 |
|
|
||||||
| 15% | `#ffffff26` | 按压态 |
|
|
||||||
| 20% | `#ffffff33` | 浅遮罩 |
|
|
||||||
| 25% | `#ffffff40` | 中遮罩 |
|
|
||||||
| 50% | `#ffffff80` | 半透明 |
|
|
||||||
| 70% | `#ffffffb2` | 深遮罩 |
|
|
||||||
| 80% | `#ffffffcc` | 悬停态 |
|
|
||||||
| 90% | `#ffffffe5` | tooltip |
|
|
||||||
| 95% | `#fffffff2` | 弹窗 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Color Palette Rules (MANDATORY)
|
|
||||||
|
|
||||||
### Strict Palette Adherence
|
|
||||||
|
|
||||||
**Use ONLY the provided color palette. Do NOT create or modify colors.**
|
|
||||||
|
|
||||||
- All colors must come from the user-provided palette
|
|
||||||
- Do NOT use colors outside the palette
|
|
||||||
- Do NOT modify palette colors (brightness, saturation, mixing)
|
|
||||||
- **Only exception**: Add transparency using the `transparency` property (0-100)
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Correct: Using palette colors
|
|
||||||
slide.addShape(pres.shapes.RECTANGLE, { fill: { color: theme.primary } });
|
|
||||||
slide.addText("Title", { color: theme.accent });
|
|
||||||
|
|
||||||
// Wrong: Colors outside palette
|
|
||||||
slide.addShape(pres.shapes.RECTANGLE, { fill: { color: "1a1a2e" } });
|
|
||||||
```
|
|
||||||
|
|
||||||
### No Gradients
|
|
||||||
|
|
||||||
**Gradients are prohibited. Use solid colors only.**
|
|
||||||
|
|
||||||
### No Animations
|
|
||||||
|
|
||||||
**Animations and transitions are prohibited.** All slides must be static.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Font Reference
|
|
||||||
|
|
||||||
### 推荐字体
|
|
||||||
|
|
||||||
| 语言 | 默认字体 | 可选字体 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| **中文** | 微软雅黑 (Microsoft YaHei) | — |
|
|
||||||
| **英文** | Arial | Georgia, Calibri, Cambria, Trebuchet MS |
|
|
||||||
|
|
||||||
- 中英文混排时:中文使用微软雅黑,英文使用所选字体
|
|
||||||
- 优先使用系统自带字体,确保跨平台兼容
|
|
||||||
- 标题和正文可使用不同字体配对(如 Georgia + Calibri)
|
|
||||||
|
|
||||||
### Recommended Font Pairings
|
|
||||||
|
|
||||||
| Header Font | Body Font |
|
|
||||||
|-------------|-----------|
|
|
||||||
| Georgia | Calibri |
|
|
||||||
| Arial Black | Arial |
|
|
||||||
| Calibri | Calibri Light |
|
|
||||||
| Cambria | Calibri |
|
|
||||||
| Trebuchet MS | Calibri |
|
|
||||||
| Impact | Arial |
|
|
||||||
| Palatino | Garamond |
|
|
||||||
| Consolas | Calibri |
|
|
||||||
|
|
||||||
**Choose an interesting font pairing** — don't default to Arial for everything. Pick a header font with personality and pair it with a clean body font.
|
|
||||||
|
|
||||||
### No Bold for Body Text
|
|
||||||
|
|
||||||
**Plain body text and caption/legend text must NOT use bold.**
|
|
||||||
|
|
||||||
- Body paragraphs, descriptions → normal weight
|
|
||||||
- Captions, legends, footnotes → normal weight
|
|
||||||
- Reserve bold for titles and headings only
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Correct
|
|
||||||
slide.addText("Main Title", { bold: true, fontSize: 36, fontFace: "Arial" });
|
|
||||||
slide.addText("Body text here.", { bold: false, fontSize: 14, fontFace: "Arial" });
|
|
||||||
|
|
||||||
// Wrong
|
|
||||||
slide.addText("Body text here.", { bold: true, fontSize: 14 });
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Style Recipes — 视觉风格系统
|
|
||||||
|
|
||||||
同一套设计可通过调整圆角(rectRadius)和间距呈现4种不同风格。根据场景选择合适的风格配方。
|
|
||||||
|
|
||||||
> **单位说明**: PptxGenJS 使用英寸(inch)作为单位。幻灯片尺寸为 10" x 5.625" (LAYOUT_16x9)
|
|
||||||
|
|
||||||
### 风格概览
|
|
||||||
|
|
||||||
| 风格 | 圆角范围 | 间距范围 | 适合场景 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **Sharp & Compact** | 0 ~ 0.05" | 紧凑 | 数据密集型、表格、专业报告 |
|
|
||||||
| **Soft & Balanced** | 0.08" ~ 0.12" | 适中 | 企业汇报、商务演示、通用PPT |
|
|
||||||
| **Rounded & Spacious** | 0.15" ~ 0.25" | 宽松 | 产品介绍、营销演示、创意展示 |
|
|
||||||
| **Pill & Airy** | 0.3" ~ 0.5" | 通透 | 品牌展示、发布会、高端演示 |
|
|
||||||
|
|
||||||
### Sharp & Compact(锐利紧凑)
|
|
||||||
|
|
||||||
**视觉特征**: 方正、信息密度高、专业严肃感。
|
|
||||||
|
|
||||||
| 类别 | 值 (英寸) | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 圆角-小 | 0" | 完全直角 |
|
|
||||||
| 圆角-中 | 0.03" | 微圆角 |
|
|
||||||
| 圆角-大 | 0.05" | 小圆角 |
|
|
||||||
| 元素内边距 | 0.1" ~ 0.15" | 紧凑 |
|
|
||||||
| 元素间距 | 0.1" ~ 0.2" | 紧凑 |
|
|
||||||
| 页面边距 | 0.3" | 较窄 |
|
|
||||||
| 区块间距 | 0.25" ~ 0.35" | 紧凑 |
|
|
||||||
|
|
||||||
### Soft & Balanced(柔和均衡)
|
|
||||||
|
|
||||||
**视觉特征**: 适中的圆角、舒适的留白、专业又不失亲和。
|
|
||||||
|
|
||||||
| 类别 | 值 (英寸) | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 圆角-小 | 0.05" | 小圆角 |
|
|
||||||
| 圆角-中 | 0.08" | 中等圆角 |
|
|
||||||
| 圆角-大 | 0.12" | 较大圆角 |
|
|
||||||
| 元素内边距 | 0.15" ~ 0.2" | 适中 |
|
|
||||||
| 元素间距 | 0.15" ~ 0.25" | 适中 |
|
|
||||||
| 页面边距 | 0.4" | 标准 |
|
|
||||||
| 区块间距 | 0.35" ~ 0.5" | 适中 |
|
|
||||||
|
|
||||||
### Rounded & Spacious(圆润宽松)
|
|
||||||
|
|
||||||
**视觉特征**: 大圆角、充裕留白、友好亲切、现代感。
|
|
||||||
|
|
||||||
| 类别 | 值 (英寸) | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 圆角-小 | 0.1" | 中等圆角 |
|
|
||||||
| 圆角-中 | 0.15" | 大圆角 |
|
|
||||||
| 圆角-大 | 0.25" | 很大圆角 |
|
|
||||||
| 元素内边距 | 0.2" ~ 0.3" | 宽松 |
|
|
||||||
| 元素间距 | 0.25" ~ 0.4" | 宽松 |
|
|
||||||
| 页面边距 | 0.5" | 较宽 |
|
|
||||||
| 区块间距 | 0.5" ~ 0.7" | 宽松 |
|
|
||||||
|
|
||||||
### Pill & Airy(胶囊通透)
|
|
||||||
|
|
||||||
**视觉特征**: 全圆角胶囊形、大量留白、轻盈通透、品牌展示感强。
|
|
||||||
|
|
||||||
| 类别 | 值 (英寸) | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 圆角-小 | 0.2" | 大圆角 |
|
|
||||||
| 圆角-中 | 0.3" | 胶囊形 |
|
|
||||||
| 圆角-大 | 0.5" | 完全胶囊 |
|
|
||||||
| 元素内边距 | 0.25" ~ 0.4" | 通透 |
|
|
||||||
| 元素间距 | 0.3" ~ 0.5" | 通透 |
|
|
||||||
| 页面边距 | 0.6" | 宽 |
|
|
||||||
| 区块间距 | 0.6" ~ 0.9" | 通透 |
|
|
||||||
|
|
||||||
### 组件级风格映射表
|
|
||||||
|
|
||||||
| 组件 | Sharp | Soft | Rounded | Pill |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| **按钮/标签** | rectRadius: 0 | rectRadius: 0.05 | rectRadius: 0.1 | rectRadius: 0.2 |
|
|
||||||
| **卡片/容器** | rectRadius: 0.03 | rectRadius: 0.1 | rectRadius: 0.2 | rectRadius: 0.3 |
|
|
||||||
| **图片容器** | rectRadius: 0 | rectRadius: 0.08 | rectRadius: 0.15 | rectRadius: 0.25 |
|
|
||||||
| **输入框形状** | rectRadius: 0 | rectRadius: 0.05 | rectRadius: 0.1 | rectRadius: 0.2 |
|
|
||||||
| **徽章/Badge** | rectRadius: 0.02 | rectRadius: 0.05 | rectRadius: 0.08 | rectRadius: 0.15 |
|
|
||||||
| **头像框** | rectRadius: 0 | rectRadius: 0.1 | rectRadius: 0.2 | rectRadius: 0.5 (圆形) |
|
|
||||||
|
|
||||||
#### PptxGenJS 圆角示例
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Sharp 风格卡片
|
|
||||||
slide.addShape("rect", {
|
|
||||||
x: 0.5, y: 1, w: 4, h: 2.5,
|
|
||||||
fill: { color: "F5F5F5" },
|
|
||||||
rectRadius: 0.03
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rounded 风格卡片
|
|
||||||
slide.addShape("rect", {
|
|
||||||
x: 0.5, y: 1, w: 4, h: 2.5,
|
|
||||||
fill: { color: "F5F5F5" },
|
|
||||||
rectRadius: 0.2
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pill 风格按钮 (高度0.4"时,rectRadius设为0.2"即为胶囊形)
|
|
||||||
slide.addShape("rect", {
|
|
||||||
x: 3, y: 4, w: 2, h: 0.4,
|
|
||||||
fill: { color: "4A90D9" },
|
|
||||||
rectRadius: 0.2
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 混搭原则
|
|
||||||
|
|
||||||
#### 1. 外层容器 >= 内层圆角
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// 正确:外 > 内
|
|
||||||
card: rectRadius: 0.2
|
|
||||||
button: rectRadius: 0.1
|
|
||||||
|
|
||||||
// 错误:内 > 外 → 视觉溢出感
|
|
||||||
card: rectRadius: 0.1
|
|
||||||
button: rectRadius: 0.2
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. 信息密度决定间距
|
|
||||||
|
|
||||||
| 区域类型 | 推荐风格 |
|
|
||||||
|---|---|
|
|
||||||
| 数据展示区 | Sharp / Soft(紧凑间距) |
|
|
||||||
| 内容浏览区 | Rounded / Pill(宽松间距) |
|
|
||||||
| 标题区域 | Soft / Rounded(适中间距) |
|
|
||||||
|
|
||||||
#### 3. 圆角与元素高度的关系
|
|
||||||
|
|
||||||
| 元素高度 | Sharp | Soft | Rounded | Pill |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| 小 (< 0.3") | 0" | 0.03" | 0.08" | 高度/2 |
|
|
||||||
| 中 (0.3" ~ 0.6") | 0.02" | 0.05" | 0.12" | 高度/2 |
|
|
||||||
| 大 (0.6" ~ 1.2") | 0.03" | 0.08" | 0.2" | 0.3" |
|
|
||||||
| 超大 (> 1.2") | 0.05" | 0.12" | 0.25" | 0.4" |
|
|
||||||
|
|
||||||
> **Pill风格提示**: 要实现完美胶囊形,设置 `rectRadius = 元素高度 / 2`
|
|
||||||
|
|
||||||
### Typography 排版规范 (PPT)
|
|
||||||
|
|
||||||
| 用途 | 字号 (pt) | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| 注释/来源 | 10 ~ 12 | 最小可读字号 |
|
|
||||||
| 正文/描述 | 14 ~ 16 | 标准正文 |
|
|
||||||
| 副标题 | 18 ~ 22 | 次要标题 |
|
|
||||||
| 标题 | 28 ~ 36 | 页面标题 |
|
|
||||||
| 大标题 | 44 ~ 60 | 封面/章节标题 |
|
|
||||||
| 数据高亮 | 60 ~ 96 | 关键数字展示 |
|
|
||||||
|
|
||||||
### Spacing 间距规范 (PPT)
|
|
||||||
|
|
||||||
基于10" x 5.625"幻灯片尺寸:
|
|
||||||
|
|
||||||
| 用途 | 推荐值 (英寸) |
|
|
||||||
|---|---|
|
|
||||||
| 图标与文字间距 | 0.08" ~ 0.15" |
|
|
||||||
| 列表项间距 | 0.15" ~ 0.25" |
|
|
||||||
| 卡片内边距 | 0.2" ~ 0.4" |
|
|
||||||
| 元素组间距 | 0.3" ~ 0.5" |
|
|
||||||
| 页面安全边距 | 0.4" ~ 0.6" |
|
|
||||||
| 主要区块间距 | 0.5" ~ 0.8" |
|
|
||||||
|
|
||||||
### 快速选择指南
|
|
||||||
|
|
||||||
| 演示类型 | 推荐风格 | 原因 |
|
|
||||||
|---|---|---|
|
|
||||||
| 财务/数据报告 | Sharp & Compact | 信息密度高,专业严谨 |
|
|
||||||
| 企业汇报/商务 | Soft & Balanced | 平衡专业与友好 |
|
|
||||||
| 产品介绍/营销 | Rounded & Spacious | 现代感,亲切感 |
|
|
||||||
| 发布会/品牌展示 | Pill & Airy | 高端感,视觉冲击 |
|
|
||||||
| 培训/教育 | Soft / Rounded | 清晰易读,友好 |
|
|
||||||
| 技术分享 | Sharp / Soft | 专业,信息清晰 |
|
|
||||||
392
skills/pptx-generator/references/design-system.md
Normal file
392
skills/pptx-generator/references/design-system.md
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
# Design System
|
||||||
|
|
||||||
|
## Color Palette Reference
|
||||||
|
|
||||||
|
| # | Name | Colors | Style | Use Cases | Tips |
|
||||||
|
|---|------|--------|-------|-----------|------|
|
||||||
|
| 1 | Modern & Wellness | `#006d77` `#83c5be` `#edf6f9` `#ffddd2` `#e29578` | Fresh, soothing | Healthcare, counseling, skincare, yoga/spa | Deep teal for titles, light pink for background |
|
||||||
|
| 2 | Business & Authority | `#2b2d42` `#8d99ae` `#edf2f4` `#ef233c` `#d90429` | Formal, classic | Annual reports, financial analysis, corporate intro, government | Deep blue for professionalism, bright red to highlight data |
|
||||||
|
| 3 | Nature & Outdoors | `#606c38` `#283618` `#fefae0` `#dda15e` `#bc6c25` | Grounded, earthy | Outdoor gear, environmental, agriculture, historical culture | Dark green base, cream text |
|
||||||
|
| 4 | Vintage & Academic | `#780000` `#c1121f` `#fdf0d5` `#003049` `#669bbc` | Classic, scholarly | Academic lectures, history reviews, museums, heritage brands | Strong contrast between deep red and deep blue |
|
||||||
|
| 5 | Soft & Creative | `#cdb4db` `#ffc8dd` `#ffafcc` `#bde0fe` `#a2d2ff` | Dreamy, candy-toned | Mother & baby, desserts, women's fashion, kindergarten | Use dark gray or black for text |
|
||||||
|
| 6 | Bohemian | `#ccd5ae` `#e9edc9` `#fefae0` `#faedcd` `#d4a373` | Gentle, muted | Wedding planning, home decor, organic food, slow living | Cream background, green-brown accents |
|
||||||
|
| 7 | Vibrant & Tech | `#8ecae6` `#219ebc` `#023047` `#ffb703` `#fb8500` | High energy, sporty | Sports events, gyms, startup pitches, youth education | Deep blue for stability, orange as focal accent |
|
||||||
|
| 8 | Craft & Artisan | `#7f5539` `#a68a64` `#ede0d4` `#656d4a` `#414833` | Rustic, coffee-toned | Coffee shops, handicrafts, traditional culture, bakery | Suited for paper/leather textures |
|
||||||
|
| 9 | Tech & Night | `#000814` `#001d3d` `#003566` `#ffc300` `#ffd60a` | Deep, luminous | Tech launches, astronomy, night economy, luxury automobiles | Must use dark mode |
|
||||||
|
| 10 | Education & Charts | `#264653` `#2a9d8f` `#e9c46a` `#f4a261` `#e76f51` | Clear, logical | Statistical reports, education, market analysis, general business | Perfect chart color scheme |
|
||||||
|
| 11 | Forest & Eco | `#dad7cd` `#a3b18a` `#588157` `#3a5a40` `#344e41` | Monochrome gradient, forest | Landscape design, ESG reports, environmental causes, botanical | Monochrome palette is safe and cohesive |
|
||||||
|
| 12 | Elegant & Fashion | `#edafb8` `#f7e1d7` `#dedbd2` `#b0c4b1` `#4a5759` | Muted, Morandi tones | Haute couture, art galleries, beauty brands, magazine style | Negative space is key |
|
||||||
|
| 13 | Art & Food | `#335c67` `#fff3b0` `#e09f3e` `#9e2a2b` `#540b0e` | Rich, vintage-poster | Food documentaries, art exhibitions, ethnic themes, vintage restaurants | Works well with large color blocks |
|
||||||
|
| 14 | Luxury & Mysterious | `#22223b` `#4a4e69` `#9a8c98` `#c9ada7` `#f2e9e4` | Cool, purple-toned | Jewelry showcases, hotel management, high-end consulting, psychology | Purple evokes premium atmosphere |
|
||||||
|
| 15 | Pure Tech Blue | `#03045e` `#0077b6` `#00b4d8` `#90e0ef` `#caf0f8` | Futuristic, clean | Cloud/AI, water/ocean, hospitals, clean energy | Deep ocean to sky gradient |
|
||||||
|
| 16 | Coastal Coral | `#0081a7` `#00afb9` `#fdfcdc` `#fed9b7` `#f07167` | Refreshing, summery | Travel, summer events, beverage brands, ocean themes | Teal and coral as complementary focal colors |
|
||||||
|
| 17 | Vibrant Orange Mint | `#ff9f1c` `#ffbf69` `#ffffff` `#cbf3f0` `#2ec4b6` | Bright, cheerful | Children's events, promotional posters, FMCG, social media | Orange grabs attention, mint feels fresh |
|
||||||
|
| 18 | Platinum White Gold | `#0a0a0a` `#0070F3` `#D4AF37` `#f5f5f5` `#ffffff` | Premium, professional | Agent products, corporate websites, fintech, luxury brands | White-gold base, blue for action, gold for emphasis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Agent Design System — Full Color Scale
|
||||||
|
|
||||||
|
Based on the Platinum White-Gold Theme design tokens. Provides complete color scales for fine-grained design work.
|
||||||
|
|
||||||
|
#### White Scale (Backgrounds & Light Surfaces)
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| white-0 | `#ffffff` | Primary background |
|
||||||
|
| white-50 | `#fefefe` | Slightly warm white |
|
||||||
|
| white-75 | `#fcfcfc` | Near-white |
|
||||||
|
| white-100 | `#fafafa` | Secondary background |
|
||||||
|
| white-200 | `#f7f7f7` | Card background |
|
||||||
|
| white-300 | `#f5f5f5` | Tertiary background |
|
||||||
|
| white-400 | `#f0f0f0` | Separator zones |
|
||||||
|
| white-500 | `#ebebeb` | Light border |
|
||||||
|
| white-600 | `#e5e5e5` | Disabled background |
|
||||||
|
| white-700 | `#e0e0e0` | Deep white-gray |
|
||||||
|
| white-800 | `#d9d9d9` | Placeholder |
|
||||||
|
| white-900 | `#d4d4d4` | Divider lines |
|
||||||
|
| white-1000 | `#cccccc` | Deepest white |
|
||||||
|
|
||||||
|
#### Gold Scale (Platinum Business Accent)
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| gold-25 | `#FFFDF5` | Extremely light gold background |
|
||||||
|
| gold-50 | `#FEF9E7` | Light gold background |
|
||||||
|
| gold-75 | `#FCF3D0` | Pale gold highlight |
|
||||||
|
| gold-100 | `#FAECB8` | Gold hover state |
|
||||||
|
| gold-200 | `#F5DC8A` | Bright gold accent |
|
||||||
|
| gold-300 | `#E8C860` | Gold hover |
|
||||||
|
| gold-400 | `#D4AF37` | **Primary gold (core)** |
|
||||||
|
| gold-500 | `#B8972E` | Gold text |
|
||||||
|
| gold-600 | `#9A7E26` | Deep gold accent |
|
||||||
|
| gold-700 | `#7C651E` | Dark gold border |
|
||||||
|
| gold-800 | `#5E4C16` | Deep gold background |
|
||||||
|
| gold-900 | `#40330F` | Very deep gold |
|
||||||
|
| gold-1000 | `#221A08` | Black gold |
|
||||||
|
|
||||||
|
#### Blue Scale (Primary Action Color)
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| blue-25 | `#F0F7FF` | Extremely light blue background |
|
||||||
|
| blue-50 | `#E0EFFF` | Info alert background |
|
||||||
|
| blue-75 | `#C2DFFF` | Light blue highlight |
|
||||||
|
| blue-100 | `#A3CFFF` | Disabled blue |
|
||||||
|
| blue-200 | `#66AFFF` | Bright blue |
|
||||||
|
| blue-300 | `#338FFF` | Blue hover |
|
||||||
|
| blue-400 | `#0070F3` | **Primary blue (core)** |
|
||||||
|
| blue-500 | `#005FCC` | Blue text |
|
||||||
|
| blue-600 | `#004FA6` | Deep blue accent |
|
||||||
|
| blue-700 | `#003F80` | Dark blue border |
|
||||||
|
| blue-800 | `#002F5A` | Deep blue background |
|
||||||
|
| blue-900 | `#001F3D` | Very deep blue |
|
||||||
|
| blue-1000 | `#001026` | Black blue |
|
||||||
|
|
||||||
|
#### Gray Scale (Text & Neutral Colors)
|
||||||
|
|
||||||
|
| Token | Value | Usage |
|
||||||
|
|-------|-------|-------|
|
||||||
|
| gray-0 | `#ffffff` | White |
|
||||||
|
| gray-50 | `#fafafa` | Extremely light gray |
|
||||||
|
| gray-75 | `#f5f5f5` | Light gray background |
|
||||||
|
| gray-100 | `#ededed` | Light divider |
|
||||||
|
| gray-200 | `#d4d4d4` | Light border |
|
||||||
|
| gray-300 | `#a3a3a3` | Quaternary text |
|
||||||
|
| gray-400 | `#737373` | Tertiary text |
|
||||||
|
| gray-500 | `#525252` | Secondary text |
|
||||||
|
| gray-600 | `#404040` | Dark gray |
|
||||||
|
| gray-700 | `#2e2e2e` | Dark background |
|
||||||
|
| gray-800 | `#1f1f1f` | Deep background |
|
||||||
|
| gray-900 | `#141414` | Very deep background |
|
||||||
|
| gray-1000 | `#0a0a0a` | **Primary text (core)** |
|
||||||
|
|
||||||
|
#### Opacity Values
|
||||||
|
|
||||||
|
##### Opacity Black
|
||||||
|
|
||||||
|
| Opacity | Value | Usage |
|
||||||
|
|---------|-------|-------|
|
||||||
|
| 0% | `#0a0a0a00` | Fully transparent |
|
||||||
|
| 2% | `#0a0a0a05` | Subtle overlay |
|
||||||
|
| 4% | `#0a0a0a0a` | Secondary interactive background |
|
||||||
|
| 8% | `#0a0a0a14` | Border / divider |
|
||||||
|
| 15% | `#0a0a0a26` | Pressed state |
|
||||||
|
| 20% | `#0a0a0a33` | Light overlay |
|
||||||
|
| 25% | `#0a0a0a40` | Medium overlay |
|
||||||
|
| 50% | `#0a0a0a80` | Semi-transparent |
|
||||||
|
| 70% | `#0a0a0ab2` | Deep overlay |
|
||||||
|
| 80% | `#0a0a0acc` | Hover state |
|
||||||
|
| 90% | `#0a0a0ae5` | Tooltip |
|
||||||
|
| 95% | `#0a0a0af2` | Modal |
|
||||||
|
|
||||||
|
##### Opacity White
|
||||||
|
|
||||||
|
| Opacity | Value | Usage |
|
||||||
|
|---------|-------|-------|
|
||||||
|
| 0% | `#ffffff00` | Fully transparent |
|
||||||
|
| 2% | `#ffffff05` | Subtle overlay |
|
||||||
|
| 4% | `#ffffff0a` | Secondary interactive background |
|
||||||
|
| 8% | `#ffffff12` | Border / divider |
|
||||||
|
| 15% | `#ffffff26` | Pressed state |
|
||||||
|
| 20% | `#ffffff33` | Light overlay |
|
||||||
|
| 25% | `#ffffff40` | Medium overlay |
|
||||||
|
| 50% | `#ffffff80` | Semi-transparent |
|
||||||
|
| 70% | `#ffffffb2` | Deep overlay |
|
||||||
|
| 80% | `#ffffffcc` | Hover state |
|
||||||
|
| 90% | `#ffffffe5` | Tooltip |
|
||||||
|
| 95% | `#fffffff2` | Modal |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Color Palette Rules (MANDATORY)
|
||||||
|
|
||||||
|
### Strict Palette Adherence
|
||||||
|
|
||||||
|
**Use ONLY the provided color palette. Do NOT create or modify colors.**
|
||||||
|
|
||||||
|
- All colors must come from the user-provided palette
|
||||||
|
- Do NOT use colors outside the palette
|
||||||
|
- Do NOT modify palette colors (brightness, saturation, mixing)
|
||||||
|
- **Only exception**: Add transparency using the `transparency` property (0-100)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Correct: Using palette colors
|
||||||
|
slide.addShape(pres.shapes.RECTANGLE, { fill: { color: theme.primary } });
|
||||||
|
slide.addText("Title", { color: theme.accent });
|
||||||
|
|
||||||
|
// Wrong: Colors outside palette
|
||||||
|
slide.addShape(pres.shapes.RECTANGLE, { fill: { color: "1a1a2e" } });
|
||||||
|
```
|
||||||
|
|
||||||
|
### No Gradients
|
||||||
|
|
||||||
|
**Gradients are prohibited. Use solid colors only.**
|
||||||
|
|
||||||
|
### No Animations
|
||||||
|
|
||||||
|
**Animations and transitions are prohibited.** All slides must be static.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Font Reference
|
||||||
|
|
||||||
|
### Recommended Fonts
|
||||||
|
|
||||||
|
| Language | Default Font | Alternatives |
|
||||||
|
|----------|-------------|--------------|
|
||||||
|
| **Chinese** | Microsoft YaHei | — |
|
||||||
|
| **English** | Arial | Georgia, Calibri, Cambria, Trebuchet MS |
|
||||||
|
|
||||||
|
- For mixed Chinese-English content: use Microsoft YaHei for Chinese, the chosen font for English
|
||||||
|
- Prefer system fonts for cross-platform compatibility
|
||||||
|
- Titles and body text can use different font pairings (e.g. Georgia + Calibri)
|
||||||
|
|
||||||
|
### Recommended Font Pairings
|
||||||
|
|
||||||
|
| Header Font | Body Font |
|
||||||
|
|-------------|-----------|
|
||||||
|
| Georgia | Calibri |
|
||||||
|
| Arial Black | Arial |
|
||||||
|
| Calibri | Calibri Light |
|
||||||
|
| Cambria | Calibri |
|
||||||
|
| Trebuchet MS | Calibri |
|
||||||
|
| Impact | Arial |
|
||||||
|
| Palatino | Garamond |
|
||||||
|
| Consolas | Calibri |
|
||||||
|
|
||||||
|
**Choose an interesting font pairing** — don't default to Arial for everything. Pick a header font with personality and pair it with a clean body font.
|
||||||
|
|
||||||
|
### No Bold for Body Text
|
||||||
|
|
||||||
|
**Plain body text and caption/legend text must NOT use bold.**
|
||||||
|
|
||||||
|
- Body paragraphs, descriptions → normal weight
|
||||||
|
- Captions, legends, footnotes → normal weight
|
||||||
|
- Reserve bold for titles and headings only
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Correct
|
||||||
|
slide.addText("Main Title", { bold: true, fontSize: 36, fontFace: "Arial" });
|
||||||
|
slide.addText("Body text here.", { bold: false, fontSize: 14, fontFace: "Arial" });
|
||||||
|
|
||||||
|
// Wrong
|
||||||
|
slide.addText("Body text here.", { bold: true, fontSize: 14 });
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Style Recipes
|
||||||
|
|
||||||
|
The same design can be rendered in 4 distinct visual styles by adjusting corner radius (`rectRadius`) and spacing. Choose the style recipe that fits the presentation tone.
|
||||||
|
|
||||||
|
> **Unit note**: PptxGenJS uses inches. Slide dimensions are 10" x 5.625" (LAYOUT_16x9).
|
||||||
|
|
||||||
|
### Style Overview
|
||||||
|
|
||||||
|
| Style | Corner Radius | Spacing | Best For |
|
||||||
|
|-------|--------------|---------|----------|
|
||||||
|
| **Sharp & Compact** | 0 ~ 0.05" | Tight | Data-dense, tables, professional reports |
|
||||||
|
| **Soft & Balanced** | 0.08" ~ 0.12" | Moderate | Corporate, business presentations, general use |
|
||||||
|
| **Rounded & Spacious** | 0.15" ~ 0.25" | Relaxed | Product intros, marketing, creative showcases |
|
||||||
|
| **Pill & Airy** | 0.3" ~ 0.5" | Open | Brand showcases, launch events, premium presentations |
|
||||||
|
|
||||||
|
### Sharp & Compact
|
||||||
|
|
||||||
|
**Visual character**: Geometric, high information density, formal and serious.
|
||||||
|
|
||||||
|
| Category | Value (inches) | Notes |
|
||||||
|
|----------|---------------|-------|
|
||||||
|
| Corner radius — small | 0" | Full right angle |
|
||||||
|
| Corner radius — medium | 0.03" | Micro-rounded |
|
||||||
|
| Corner radius — large | 0.05" | Slight rounding |
|
||||||
|
| Element padding | 0.1" ~ 0.15" | Compact |
|
||||||
|
| Element gap | 0.1" ~ 0.2" | Compact |
|
||||||
|
| Page margin | 0.3" | Narrow |
|
||||||
|
| Block gap | 0.25" ~ 0.35" | Compact |
|
||||||
|
|
||||||
|
### Soft & Balanced
|
||||||
|
|
||||||
|
**Visual character**: Moderate rounding, comfortable whitespace, professional yet approachable.
|
||||||
|
|
||||||
|
| Category | Value (inches) | Notes |
|
||||||
|
|----------|---------------|-------|
|
||||||
|
| Corner radius — small | 0.05" | Slight rounding |
|
||||||
|
| Corner radius — medium | 0.08" | Medium rounding |
|
||||||
|
| Corner radius — large | 0.12" | Larger rounding |
|
||||||
|
| Element padding | 0.15" ~ 0.2" | Moderate |
|
||||||
|
| Element gap | 0.15" ~ 0.25" | Moderate |
|
||||||
|
| Page margin | 0.4" | Standard |
|
||||||
|
| Block gap | 0.35" ~ 0.5" | Moderate |
|
||||||
|
|
||||||
|
### Rounded & Spacious
|
||||||
|
|
||||||
|
**Visual character**: Large corners, generous whitespace, friendly and modern.
|
||||||
|
|
||||||
|
| Category | Value (inches) | Notes |
|
||||||
|
|----------|---------------|-------|
|
||||||
|
| Corner radius — small | 0.1" | Medium rounding |
|
||||||
|
| Corner radius — medium | 0.15" | Large rounding |
|
||||||
|
| Corner radius — large | 0.25" | Very large rounding |
|
||||||
|
| Element padding | 0.2" ~ 0.3" | Relaxed |
|
||||||
|
| Element gap | 0.25" ~ 0.4" | Relaxed |
|
||||||
|
| Page margin | 0.5" | Wide |
|
||||||
|
| Block gap | 0.5" ~ 0.7" | Relaxed |
|
||||||
|
|
||||||
|
### Pill & Airy
|
||||||
|
|
||||||
|
**Visual character**: Full pill-shaped corners, abundant whitespace, light and open feel, strong brand presence.
|
||||||
|
|
||||||
|
| Category | Value (inches) | Notes |
|
||||||
|
|----------|---------------|-------|
|
||||||
|
| Corner radius — small | 0.2" | Large rounding |
|
||||||
|
| Corner radius — medium | 0.3" | Pill shape |
|
||||||
|
| Corner radius — large | 0.5" | Full pill |
|
||||||
|
| Element padding | 0.25" ~ 0.4" | Open |
|
||||||
|
| Element gap | 0.3" ~ 0.5" | Open |
|
||||||
|
| Page margin | 0.6" | Wide |
|
||||||
|
| Block gap | 0.6" ~ 0.9" | Open |
|
||||||
|
|
||||||
|
### Component Style Mapping
|
||||||
|
|
||||||
|
| Component | Sharp | Soft | Rounded | Pill |
|
||||||
|
|-----------|-------|------|---------|------|
|
||||||
|
| **Button / Tag** | rectRadius: 0 | rectRadius: 0.05 | rectRadius: 0.1 | rectRadius: 0.2 |
|
||||||
|
| **Card / Container** | rectRadius: 0.03 | rectRadius: 0.1 | rectRadius: 0.2 | rectRadius: 0.3 |
|
||||||
|
| **Image Container** | rectRadius: 0 | rectRadius: 0.08 | rectRadius: 0.15 | rectRadius: 0.25 |
|
||||||
|
| **Input Field** | rectRadius: 0 | rectRadius: 0.05 | rectRadius: 0.1 | rectRadius: 0.2 |
|
||||||
|
| **Badge** | rectRadius: 0.02 | rectRadius: 0.05 | rectRadius: 0.08 | rectRadius: 0.15 |
|
||||||
|
| **Avatar Frame** | rectRadius: 0 | rectRadius: 0.1 | rectRadius: 0.2 | rectRadius: 0.5 (circle) |
|
||||||
|
|
||||||
|
#### PptxGenJS Corner Radius Examples
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Sharp style card
|
||||||
|
slide.addShape("rect", {
|
||||||
|
x: 0.5, y: 1, w: 4, h: 2.5,
|
||||||
|
fill: { color: "F5F5F5" },
|
||||||
|
rectRadius: 0.03
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rounded style card
|
||||||
|
slide.addShape("rect", {
|
||||||
|
x: 0.5, y: 1, w: 4, h: 2.5,
|
||||||
|
fill: { color: "F5F5F5" },
|
||||||
|
rectRadius: 0.2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pill style button (height 0.4", rectRadius 0.2" = perfect pill)
|
||||||
|
slide.addShape("rect", {
|
||||||
|
x: 3, y: 4, w: 2, h: 0.4,
|
||||||
|
fill: { color: "4A90D9" },
|
||||||
|
rectRadius: 0.2
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mixing Rules
|
||||||
|
|
||||||
|
#### 1. Outer container corner >= inner element corner
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Correct: outer > inner
|
||||||
|
card: rectRadius: 0.2
|
||||||
|
button: rectRadius: 0.1
|
||||||
|
|
||||||
|
// Wrong: inner > outer → visual overflow effect
|
||||||
|
card: rectRadius: 0.1
|
||||||
|
button: rectRadius: 0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Information density drives spacing
|
||||||
|
|
||||||
|
| Zone Type | Recommended Style |
|
||||||
|
|-----------|------------------|
|
||||||
|
| Data display zone | Sharp / Soft (compact spacing) |
|
||||||
|
| Content browsing zone | Rounded / Pill (relaxed spacing) |
|
||||||
|
| Title zone | Soft / Rounded (moderate spacing) |
|
||||||
|
|
||||||
|
#### 3. Corner radius vs element height
|
||||||
|
|
||||||
|
| Element Height | Sharp | Soft | Rounded | Pill |
|
||||||
|
|---------------|-------|------|---------|------|
|
||||||
|
| Small (< 0.3") | 0" | 0.03" | 0.08" | height/2 |
|
||||||
|
| Medium (0.3" ~ 0.6") | 0.02" | 0.05" | 0.12" | height/2 |
|
||||||
|
| Large (0.6" ~ 1.2") | 0.03" | 0.08" | 0.2" | 0.3" |
|
||||||
|
| Extra large (> 1.2") | 0.05" | 0.12" | 0.25" | 0.4" |
|
||||||
|
|
||||||
|
> **Pill tip**: For a perfect pill shape, set `rectRadius = element height / 2`
|
||||||
|
|
||||||
|
### Typography Scale (PPT)
|
||||||
|
|
||||||
|
| Usage | Size (pt) | Notes |
|
||||||
|
|-------|-----------|-------|
|
||||||
|
| Annotations / Sources | 10 ~ 12 | Minimum readable size |
|
||||||
|
| Body / Description | 14 ~ 16 | Standard body |
|
||||||
|
| Subtitle | 18 ~ 22 | Secondary heading |
|
||||||
|
| Title | 28 ~ 36 | Page title |
|
||||||
|
| Large Title | 44 ~ 60 | Cover / section title |
|
||||||
|
| Data Callout | 60 ~ 96 | Key number display |
|
||||||
|
|
||||||
|
### Spacing Scale (PPT)
|
||||||
|
|
||||||
|
Based on 10" x 5.625" slide dimensions:
|
||||||
|
|
||||||
|
| Usage | Recommended (inches) |
|
||||||
|
|-------|---------------------|
|
||||||
|
| Icon-to-text gap | 0.08" ~ 0.15" |
|
||||||
|
| List item spacing | 0.15" ~ 0.25" |
|
||||||
|
| Card inner padding | 0.2" ~ 0.4" |
|
||||||
|
| Element group gap | 0.3" ~ 0.5" |
|
||||||
|
| Page safe margin | 0.4" ~ 0.6" |
|
||||||
|
| Major block gap | 0.5" ~ 0.8" |
|
||||||
|
|
||||||
|
### Quick Selection Guide
|
||||||
|
|
||||||
|
| Presentation Type | Recommended Style | Reason |
|
||||||
|
|------------------|------------------|--------|
|
||||||
|
| Finance / Data reports | Sharp & Compact | High density, serious and precise |
|
||||||
|
| Corporate / Business | Soft & Balanced | Balances professionalism and approachability |
|
||||||
|
| Product intro / Marketing | Rounded & Spacious | Modern feel, friendly |
|
||||||
|
| Launch events / Brand | Pill & Airy | Premium feel, visual impact |
|
||||||
|
| Training / Education | Soft / Rounded | Clear, readable, friendly |
|
||||||
|
| Tech sharing | Sharp / Soft | Professional, information-dense |
|
||||||
@@ -130,7 +130,7 @@ Consider: Purpose (corporate/educational/creative), Audience, Tone, Content Volu
|
|||||||
4. **Section Descriptions** — Optional one-line summaries
|
4. **Section Descriptions** — Optional one-line summaries
|
||||||
5. **Visual Separators** — SVG dividers or spacing
|
5. **Visual Separators** — SVG dividers or spacing
|
||||||
6. **Decorative Elements** — Subtle accent shapes
|
6. **Decorative Elements** — Subtle accent shapes
|
||||||
7. **Page Number Badge (角标)** — **MANDATORY**
|
7. **Page Number Badge** — **MANDATORY**
|
||||||
|
|
||||||
### Design Decisions
|
### Design Decisions
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ Consider: Purpose (corporate/educational/creative), Audience, Tone, Content Volu
|
|||||||
2. **Section Title** — Always required. Clear, concise.
|
2. **Section Title** — Always required. Clear, concise.
|
||||||
3. **Intro Text** — Optional 1-2 line description.
|
3. **Intro Text** — Optional 1-2 line description.
|
||||||
4. **Decorative Elements** — SVG accent shapes (bars, lines, geometric blocks).
|
4. **Decorative Elements** — SVG accent shapes (bars, lines, geometric blocks).
|
||||||
5. **Page Number Badge (角标)** — **MANDATORY**.
|
5. **Page Number Badge** — **MANDATORY**.
|
||||||
|
|
||||||
### Design Decisions
|
### Design Decisions
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ Pick a subtype based on the content. Each content slide belongs to exactly ONE s
|
|||||||
2. **Body Content** — Text, bullets, data, or comparisons based on subtype
|
2. **Body Content** — Text, bullets, data, or comparisons based on subtype
|
||||||
3. **Visual Element** — Image, chart, icon, or SVG shape — always required
|
3. **Visual Element** — Image, chart, icon, or SVG shape — always required
|
||||||
4. **Source / Caption** — When showing data or external content
|
4. **Source / Caption** — When showing data or external content
|
||||||
5. **Page Number Badge (角标)** — **MANDATORY**
|
5. **Page Number Badge** — **MANDATORY**
|
||||||
|
|
||||||
### Design Decisions
|
### Design Decisions
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ Pick a subtype based on the content. Each content slide belongs to exactly ONE s
|
|||||||
3. **Call to Action** — Clear next steps (if applicable)
|
3. **Call to Action** — Clear next steps (if applicable)
|
||||||
4. **Contact Info** — Email, website, social handles (if provided)
|
4. **Contact Info** — Email, website, social handles (if provided)
|
||||||
5. **Decorative Elements** — SVG accents for visual consistency
|
5. **Decorative Elements** — SVG accents for visual consistency
|
||||||
6. **Page Number Badge (角标)** — **MANDATORY**
|
6. **Page Number Badge** — **MANDATORY**
|
||||||
|
|
||||||
### Design Decisions
|
### Design Decisions
|
||||||
|
|
||||||
Reference in New Issue
Block a user